mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 17:03:10 +00:00
Compare commits
137 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0943b052b9 | |||
| a896fffdb3 | |||
| 177daf903b | |||
| aa1ebdc4ed | |||
| 38058c4a70 | |||
| a7eb483479 | |||
| c76931a3b4 | |||
| b90b7e9e0c | |||
| beda53d36d | |||
| 0668f669c1 | |||
| 64250e13db | |||
| 451d38d5e7 | |||
| e11e437bbb | |||
| b8a1ad798d | |||
| 4a851c44f2 | |||
| d079796020 | |||
| f125bc88b0 | |||
| f3d535f286 | |||
| 6e8c6feec2 | |||
| 84b0444caf | |||
| 4be986ebe7 | |||
| 21872f818a | |||
| 4e7c45d758 | |||
| 0e55e5f065 | |||
| b1c5cf318a | |||
| 9d94a24862 | |||
| 6cf9684f55 | |||
| 8043a48c49 | |||
| 7d657dd533 | |||
| 1bfdec1484 | |||
| 8d5b6a53f3 | |||
| ad97102e7c | |||
| d8dc121386 | |||
| 10c3868c00 | |||
| 411ba00a82 | |||
| c1b2b62dbc | |||
| 3960cab7a7 | |||
| 34e9447e55 | |||
| 9d3295acc7 | |||
| 995cd3d84c | |||
| 919cdf0075 | |||
| d181c9cfc1 | |||
| 5943142c44 | |||
| dbc430e838 | |||
| 7413d03d31 | |||
| dd60067684 | |||
| 04e4ddf525 | |||
| 99bf6d7c75 | |||
| 99a203933c | |||
| b43d2a2040 | |||
| ba9c406def | |||
| f5f4d50cc9 | |||
| f87506b490 | |||
| f6f9a53ce5 | |||
| 61130ef54e | |||
| e8bcbd59be | |||
| d433ff7475 | |||
| 71957037d0 | |||
| bcdcdbbd4e | |||
| 1ece13f5bc | |||
| da70ba6ec7 | |||
| a832f0e4d4 | |||
| 6ab4d005a0 | |||
| 18dc935038 | |||
| bb3a534f75 | |||
| 649ba370c6 | |||
| 51732c5ce6 | |||
| 0ba7b46680 | |||
| 94dfbb7b9e | |||
| b8b7512b35 | |||
| aba6b773ce | |||
| d88fb0d356 | |||
| b8c6d55156 | |||
| d9a5481db2 | |||
| 6a8bb42c0e | |||
| 2445f001ab | |||
| c84fa9ac32 | |||
| d44a11c44e | |||
| b215cccda4 | |||
| 3eda488361 | |||
| 993a44de35 | |||
| 99465f99a1 | |||
| d42de1fe90 | |||
| d0284c62c0 | |||
| d92f3b7904 | |||
| 3e1b5ada69 | |||
| 6156fb8154 | |||
| f2753aed1e | |||
| e33d826381 | |||
| 364aa4d324 | |||
| 455b332757 | |||
| 876b895645 | |||
| daf7ed9fe6 | |||
| 3e365f83c9 | |||
| 40977ebdbe | |||
| 4a9058fc1c | |||
| dab9a21608 | |||
| a89c222399 | |||
| 1e356d2a45 | |||
| eed794c2cf | |||
| 2f82e2015b | |||
| ad599badb2 | |||
| 1e45c73f1d | |||
| 49c1fda2c1 | |||
| 32a31e464b | |||
| cddb4167e4 | |||
| 65457d8df2 | |||
| 122a6cad43 | |||
| 4c0e841425 | |||
| 92f5839aec | |||
| 30475dae67 | |||
| 3d942bd503 | |||
| f739520e52 | |||
| 0152603ddb | |||
| aa06e0eead | |||
| 2fde9a285e | |||
| b9f6eb6abb | |||
| d77c4354a6 | |||
| 21860ddf85 | |||
| 2cffa22cc2 | |||
| 985ba9bb29 | |||
| 96f23f163d | |||
| 0e7d49991a | |||
| 3e635cf0fe | |||
| 1425c66c69 | |||
| fc3b7cc75b | |||
| 2cc2dc6ceb | |||
| 7da8cedf66 | |||
| 2cf10668dd | |||
| f1342b5ff2 | |||
| a04b636a11 | |||
| e4637ad295 | |||
| 3a1e43dabe | |||
| 10da1a7bfe | |||
| 37320ca862 | |||
| 2074a5e26f | |||
| 6b14cdbf06 |
@@ -16,7 +16,7 @@ jobs:
|
|||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v1
|
uses: actions/setup-dotnet@v1
|
||||||
with:
|
with:
|
||||||
dotnet-version: 9.0.x
|
dotnet-version: 10.0.x
|
||||||
- name: Restore dependencies
|
- name: Restore dependencies
|
||||||
run: dotnet restore
|
run: dotnet restore
|
||||||
- name: Build
|
- name: Build
|
||||||
|
|||||||
@@ -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;net10.0</TargetFrameworks>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup>
|
||||||
|
<PackageId>CryptoExchange.Net.Protobuf</PackageId>
|
||||||
|
<Authors>JKorf</Authors>
|
||||||
|
<Description>Protobuf support for CryptoExchange.Net</Description>
|
||||||
|
<PackageVersion>10.0.1</PackageVersion>
|
||||||
|
<AssemblyVersion>10.0.1</AssemblyVersion>
|
||||||
|
<FileVersion>10.0.1</FileVersion>
|
||||||
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
|
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
|
||||||
|
<RepositoryType>git</RepositoryType>
|
||||||
|
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
||||||
|
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net/tree/master/CryptoExchange.Net.Protobuf</PackageProjectUrl>
|
||||||
|
<NeutralLanguage>en</NeutralLanguage>
|
||||||
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
|
<PackageIcon>icon.png</PackageIcon>
|
||||||
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
|
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<LangVersion>12.0</LangVersion>
|
||||||
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="..\CryptoExchange.Net\Icon\icon.png" Pack="true" PackagePath="\" />
|
||||||
|
<None Include="README.md" Pack="true" PackagePath="\" />
|
||||||
|
</ItemGroup>
|
||||||
|
<PropertyGroup Label="AOT" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
|
||||||
|
<IsAotCompatible>true</IsAotCompatible>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
||||||
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
|
<IncludeSymbols>true</IncludeSymbols>
|
||||||
|
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||||
|
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||||
|
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup>
|
||||||
|
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CryptoExchange.Net" Version="10.0.2" />
|
||||||
|
<PackageReference Include="protobuf-net" Version="3.2.56" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -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,52 @@
|
|||||||
|
#  CryptoExchange.Net.Proto
|
||||||
|
|
||||||
|
[](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) [](https://www.nuget.org/packages/CryptoExchange.Net.Protobuf) 
|
||||||
|
|
||||||
|
Protobuf support for CryptoExchange.Net.
|
||||||
|
|
||||||
|
## Release notes
|
||||||
|
* Version 10.0.1 - 16 Dec 2025
|
||||||
|
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
|
* Version 10.0.0 - 16 Dec 2025
|
||||||
|
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
|
* Version 9.13.0 - 10 Nov 2025
|
||||||
|
* Updated CryptoExchange.Net version to 9.13.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
|
* Version 9.12.0 - 03 Nov 2025
|
||||||
|
* Updated CryptoExchange.Net version to 9.12.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
|
* Version 9.11.1 - 30 Oct 2025
|
||||||
|
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
|
* Version 9.11.0 - 30 Oct 2025
|
||||||
|
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
|
* Version 9.10.0 - 15 Oct 2025
|
||||||
|
* Updated CryptoExchange.Net version to 9.10.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
|
* Version 9.9.0 - 06 Oct 2025
|
||||||
|
* Updated CryptoExchange.Net version to 9.9.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
|
* Version 9.8.0 - 30 Sep 2025
|
||||||
|
* Updated CryptoExchange.Net version to 9.8.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
|
* Version 9.7.0 - 01 Sep 2025
|
||||||
|
* Updated CryptoExchange.Net version to 9.7.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
|
* Version 9.6.0 - 25 Aug 2025
|
||||||
|
* Updated CryptoExchange.Net version to 9.6.0
|
||||||
|
|
||||||
|
* Version 9.5.0 - 19 Aug 2025
|
||||||
|
* Updated CryptoExchange.Net version to 9.5.0
|
||||||
|
|
||||||
|
* Version 9.4.0 - 04 Aug 2025
|
||||||
|
* Updated CryptoExchange.Net to version 9.4.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
* Updated protobuf-net package version to 3.2.56
|
||||||
|
|
||||||
|
* Version 9.3.0 - 23 Jul 2025
|
||||||
|
* Updated CryptoExchange.Net to version 9.3.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
|
* Version 9.2.0 - 14 Jul 2025
|
||||||
|
* Initial release
|
||||||
@@ -4,7 +4,6 @@ using NUnit.Framework.Legacy;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using NUnit.Framework;
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using NUnit.Framework;
|
|
||||||
using NUnit.Framework.Legacy;
|
using NUnit.Framework.Legacy;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Errors;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NUnit.Framework.Legacy;
|
using NUnit.Framework.Legacy;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
@@ -16,9 +15,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 +35,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 +70,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 +83,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 +96,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 +111,8 @@ 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>>>(),
|
HttpVersion.Version11,
|
||||||
|
new HttpResponseMessage().Headers,
|
||||||
TimeSpan.FromSeconds(1),
|
TimeSpan.FromSeconds(1),
|
||||||
null,
|
null,
|
||||||
"{}",
|
"{}",
|
||||||
@@ -120,14 +120,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 HttpRequestMessage().Headers,
|
||||||
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 +142,8 @@ 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>>>(),
|
HttpVersion.Version11,
|
||||||
|
new HttpResponseMessage().Headers,
|
||||||
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 HttpRequestMessage().Headers,
|
||||||
ResultDataSource.Server,
|
ResultDataSource.Server,
|
||||||
new TestObjectResult(),
|
new TestObjectResult(),
|
||||||
null);
|
null);
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"></PackageReference>
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.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.4.0"></PackageReference>
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0"></PackageReference>
|
<PackageReference Include="NUnit3TestAdapter" Version="6.0.0"></PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NUnit.Framework.Legacy;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
@@ -32,6 +30,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.532, 0.532)]
|
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.532, 0.532)]
|
||||||
[TestCase(0.1, 1, 0.0001, RoundingType.Down, 0.5516592, 0.5516)]
|
[TestCase(0.1, 1, 0.0001, RoundingType.Down, 0.5516592, 0.5516)]
|
||||||
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.5516592, 0.5517)]
|
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.5516592, 0.5517)]
|
||||||
|
[TestCase(0, 1, 0.000000001, RoundingType.Closest, 0.0000097232, 0.000009723)]
|
||||||
public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected)
|
public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected)
|
||||||
{
|
{
|
||||||
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
|
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
|
||||||
|
|||||||
@@ -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,15 +1,8 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NUnit.Framework.Legacy;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,23 +1,18 @@
|
|||||||
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;
|
||||||
using NUnit.Framework.Legacy;
|
using NUnit.Framework.Legacy;
|
||||||
using CryptoExchange.Net.RateLimiting;
|
using CryptoExchange.Net.RateLimiting;
|
||||||
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 +25,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 +79,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 +95,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 +133,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
client.SetResponse("{}", out var request);
|
client.SetResponse("{}", out var request);
|
||||||
|
|
||||||
await client.Api1.RequestWithParams<TestObject>(new HttpMethod(method), new Dictionary<string, object>
|
await client.Api1.RequestWithParams<TestObject>(new HttpMethod(method), new ParameterCollection
|
||||||
{
|
{
|
||||||
{ "TestParam1", "Value1" },
|
{ "TestParam1", "Value1" },
|
||||||
{ "TestParam2", 2 },
|
{ "TestParam2", 2 },
|
||||||
@@ -188,7 +181,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[TestCase("/sapi/test1", true)]
|
[TestCase("/sapi/test1", true)]
|
||||||
[TestCase("/sapi/test2", true)]
|
[TestCase("/sapi/test2", true)]
|
||||||
[TestCase("/api/test1", false)]
|
[TestCase("/api/test1", false)]
|
||||||
[TestCase("sapi/test1", false)]
|
[TestCase("sapi/test1", true)]
|
||||||
[TestCase("/sapi/", true)]
|
[TestCase("/sapi/", true)]
|
||||||
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
|
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,231 +1,234 @@
|
|||||||
using System;
|
//using CryptoExchange.Net.Objects;
|
||||||
using System.Collections.Generic;
|
//using CryptoExchange.Net.Objects.Sockets;
|
||||||
using System.Threading;
|
//using CryptoExchange.Net.Sockets;
|
||||||
using System.Threading.Tasks;
|
//using CryptoExchange.Net.Testing.Implementations;
|
||||||
using CryptoExchange.Net.Objects;
|
//using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
//using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
||||||
using CryptoExchange.Net.Sockets;
|
//using Microsoft.Extensions.Logging;
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
//using Moq;
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
//using NUnit.Framework;
|
||||||
using Microsoft.Extensions.Logging;
|
//using NUnit.Framework.Legacy;
|
||||||
using Moq;
|
//using System;
|
||||||
using Newtonsoft.Json;
|
//using System.Collections.Generic;
|
||||||
using NUnit.Framework;
|
//using System.Net.Sockets;
|
||||||
using NUnit.Framework.Legacy;
|
//using System.Text.Json;
|
||||||
|
//using System.Threading;
|
||||||
|
//using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
//namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
//{
|
||||||
[TestFixture]
|
// [TestFixture]
|
||||||
public class SocketClientTests
|
// public class SocketClientTests
|
||||||
{
|
// {
|
||||||
[TestCase]
|
// [TestCase]
|
||||||
public void SettingOptions_Should_ResultInOptionsSet()
|
// public void SettingOptions_Should_ResultInOptionsSet()
|
||||||
{
|
// {
|
||||||
//arrange
|
// //arrange
|
||||||
//act
|
// //act
|
||||||
var client = new TestSocketClient(options =>
|
// var client = new TestSocketClient(options =>
|
||||||
{
|
// {
|
||||||
options.SubOptions.ApiCredentials = new Authentication.ApiCredentials("1", "2");
|
// options.SubOptions.ApiCredentials = new Authentication.ApiCredentials("1", "2");
|
||||||
options.SubOptions.MaxSocketConnections = 1;
|
// options.SubOptions.MaxSocketConnections = 1;
|
||||||
});
|
// });
|
||||||
|
|
||||||
//assert
|
// //assert
|
||||||
ClassicAssert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
|
// ClassicAssert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
|
||||||
Assert.That(1 == client.SubClient.ApiOptions.MaxSocketConnections);
|
// Assert.That(1 == client.SubClient.ApiOptions.MaxSocketConnections);
|
||||||
}
|
// }
|
||||||
|
|
||||||
[TestCase(true)]
|
// [TestCase(true)]
|
||||||
[TestCase(false)]
|
// [TestCase(false)]
|
||||||
public void ConnectSocket_Should_ReturnConnectionResult(bool canConnect)
|
// public void ConnectSocket_Should_ReturnConnectionResult(bool canConnect)
|
||||||
{
|
// {
|
||||||
//arrange
|
// //arrange
|
||||||
var client = new TestSocketClient();
|
// var client = new TestSocketClient();
|
||||||
var socket = client.CreateSocket();
|
// var socket = client.CreateSocket();
|
||||||
socket.CanConnect = canConnect;
|
// socket.CanConnect = canConnect;
|
||||||
|
|
||||||
//act
|
// //act
|
||||||
var connectResult = client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, null));
|
// var connectResult = client.SubClient.ConnectSocketSub(
|
||||||
|
// new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
|
||||||
|
|
||||||
//assert
|
// //assert
|
||||||
Assert.That(connectResult.Success == canConnect);
|
// Assert.That(connectResult.Success == canConnect);
|
||||||
}
|
// }
|
||||||
|
|
||||||
[TestCase]
|
// [TestCase]
|
||||||
public void SocketMessages_Should_BeProcessedInDataHandlers()
|
// public void SocketMessages_Should_BeProcessedInDataHandlers()
|
||||||
{
|
// {
|
||||||
// arrange
|
// // arrange
|
||||||
var client = new TestSocketClient(options => {
|
// var client = new TestSocketClient(options => {
|
||||||
options.ReconnectInterval = TimeSpan.Zero;
|
// options.ReconnectInterval = TimeSpan.Zero;
|
||||||
});
|
// });
|
||||||
var socket = client.CreateSocket();
|
// var socket = client.CreateSocket();
|
||||||
socket.CanConnect = true;
|
// socket.CanConnect = true;
|
||||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||||
var rstEvent = new ManualResetEvent(false);
|
// var rstEvent = new ManualResetEvent(false);
|
||||||
Dictionary<string, string> result = null;
|
// Dictionary<string, string> result = null;
|
||||||
|
|
||||||
client.SubClient.ConnectSocketSub(sub);
|
// client.SubClient.ConnectSocketSub(sub);
|
||||||
|
|
||||||
var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
// var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
||||||
{
|
// {
|
||||||
result = messageEvent.Data;
|
// result = messageEvent.Data;
|
||||||
rstEvent.Set();
|
// rstEvent.Set();
|
||||||
});
|
// });
|
||||||
sub.AddSubscription(subObj);
|
// sub.AddSubscription(subObj);
|
||||||
|
|
||||||
// act
|
// // act
|
||||||
socket.InvokeMessage("{\"property\": \"123\", \"action\": \"update\", \"topic\": \"topic\"}");
|
// socket.InvokeMessage("{\"property\": \"123\", \"action\": \"update\", \"topic\": \"topic\"}");
|
||||||
rstEvent.WaitOne(1000);
|
// rstEvent.WaitOne(1000);
|
||||||
|
|
||||||
// assert
|
// // assert
|
||||||
Assert.That(result["property"] == "123");
|
// Assert.That(result["property"] == "123");
|
||||||
}
|
// }
|
||||||
|
|
||||||
[TestCase(false)]
|
// [TestCase(false)]
|
||||||
[TestCase(true)]
|
// [TestCase(true)]
|
||||||
public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
|
// public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
|
||||||
{
|
// {
|
||||||
// arrange
|
// // arrange
|
||||||
var client = new TestSocketClient(options =>
|
// var client = new TestSocketClient(options =>
|
||||||
{
|
// {
|
||||||
options.ReconnectInterval = TimeSpan.Zero;
|
// options.ReconnectInterval = TimeSpan.Zero;
|
||||||
options.SubOptions.OutputOriginalData = enabled;
|
// options.SubOptions.OutputOriginalData = enabled;
|
||||||
});
|
// });
|
||||||
var socket = client.CreateSocket();
|
// var socket = client.CreateSocket();
|
||||||
socket.CanConnect = true;
|
// socket.CanConnect = true;
|
||||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||||
var rstEvent = new ManualResetEvent(false);
|
// var rstEvent = new ManualResetEvent(false);
|
||||||
string original = null;
|
// string original = null;
|
||||||
|
|
||||||
client.SubClient.ConnectSocketSub(sub);
|
// client.SubClient.ConnectSocketSub(sub);
|
||||||
var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
// var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
||||||
{
|
// {
|
||||||
original = messageEvent.OriginalData;
|
// original = messageEvent.OriginalData;
|
||||||
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);
|
||||||
rstEvent.WaitOne(1000);
|
// rstEvent.WaitOne(1000);
|
||||||
|
|
||||||
// assert
|
// // assert
|
||||||
Assert.That(original == (enabled ? msgToSend : null));
|
// Assert.That(original == (enabled ? msgToSend : null));
|
||||||
}
|
// }
|
||||||
|
|
||||||
[TestCase()]
|
// [TestCase()]
|
||||||
public void UnsubscribingStream_Should_CloseTheSocket()
|
// public void UnsubscribingStream_Should_CloseTheSocket()
|
||||||
{
|
// {
|
||||||
// arrange
|
// // arrange
|
||||||
var client = new TestSocketClient(options =>
|
// var client = new TestSocketClient(options =>
|
||||||
{
|
// {
|
||||||
options.ReconnectInterval = TimeSpan.Zero;
|
// options.ReconnectInterval = TimeSpan.Zero;
|
||||||
});
|
// });
|
||||||
var socket = client.CreateSocket();
|
// var socket = client.CreateSocket();
|
||||||
socket.CanConnect = true;
|
// socket.CanConnect = true;
|
||||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||||
client.SubClient.ConnectSocketSub(sub);
|
// client.SubClient.ConnectSocketSub(sub);
|
||||||
|
|
||||||
var subscription = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
// var subscription = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
||||||
var ups = new UpdateSubscription(sub, subscription);
|
// var ups = new UpdateSubscription(sub, subscription);
|
||||||
sub.AddSubscription(subscription);
|
// sub.AddSubscription(subscription);
|
||||||
|
|
||||||
// act
|
// // act
|
||||||
client.UnsubscribeAsync(ups).Wait();
|
// client.UnsubscribeAsync(ups).Wait();
|
||||||
|
|
||||||
// assert
|
// // assert
|
||||||
Assert.That(socket.Connected == false);
|
// Assert.That(socket.Connected == false);
|
||||||
}
|
// }
|
||||||
|
|
||||||
[TestCase()]
|
// [TestCase()]
|
||||||
public void UnsubscribingAll_Should_CloseAllSockets()
|
// public void UnsubscribingAll_Should_CloseAllSockets()
|
||||||
{
|
// {
|
||||||
// arrange
|
// // arrange
|
||||||
var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
// var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
||||||
var socket1 = client.CreateSocket();
|
// var socket1 = client.CreateSocket();
|
||||||
var socket2 = client.CreateSocket();
|
// var socket2 = client.CreateSocket();
|
||||||
socket1.CanConnect = true;
|
// socket1.CanConnect = true;
|
||||||
socket2.CanConnect = true;
|
// socket2.CanConnect = true;
|
||||||
var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket1, null);
|
// var sub1 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket1), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||||
var sub2 = new SocketConnection(new TraceLogger(), client.SubClient, socket2, null);
|
// var sub2 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket2), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||||
client.SubClient.ConnectSocketSub(sub1);
|
// client.SubClient.ConnectSocketSub(sub1);
|
||||||
client.SubClient.ConnectSocketSub(sub2);
|
// client.SubClient.ConnectSocketSub(sub2);
|
||||||
var subscription1 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
// var subscription1 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
||||||
var subscription2 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
// var subscription2 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
||||||
|
|
||||||
sub1.AddSubscription(subscription1);
|
// sub1.AddSubscription(subscription1);
|
||||||
sub2.AddSubscription(subscription2);
|
// sub2.AddSubscription(subscription2);
|
||||||
var ups1 = new UpdateSubscription(sub1, subscription1);
|
// var ups1 = new UpdateSubscription(sub1, subscription1);
|
||||||
var ups2 = new UpdateSubscription(sub2, subscription2);
|
// var ups2 = new UpdateSubscription(sub2, subscription2);
|
||||||
|
|
||||||
// act
|
// // act
|
||||||
client.UnsubscribeAllAsync().Wait();
|
// client.UnsubscribeAllAsync().Wait();
|
||||||
|
|
||||||
// assert
|
// // assert
|
||||||
Assert.That(socket1.Connected == false);
|
// Assert.That(socket1.Connected == false);
|
||||||
Assert.That(socket2.Connected == false);
|
// Assert.That(socket2.Connected == false);
|
||||||
}
|
// }
|
||||||
|
|
||||||
[TestCase()]
|
// [TestCase()]
|
||||||
public void FailingToConnectSocket_Should_ReturnError()
|
// public void FailingToConnectSocket_Should_ReturnError()
|
||||||
{
|
// {
|
||||||
// arrange
|
// // arrange
|
||||||
var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
// var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
||||||
var socket = client.CreateSocket();
|
// var socket = client.CreateSocket();
|
||||||
socket.CanConnect = false;
|
// socket.CanConnect = false;
|
||||||
var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
// var sub1 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||||
|
|
||||||
// act
|
// // act
|
||||||
var connectResult = client.SubClient.ConnectSocketSub(sub1);
|
// var connectResult = client.SubClient.ConnectSocketSub(sub1);
|
||||||
|
|
||||||
// assert
|
// // assert
|
||||||
ClassicAssert.IsFalse(connectResult.Success);
|
// ClassicAssert.IsFalse(connectResult.Success);
|
||||||
}
|
// }
|
||||||
|
|
||||||
[TestCase()]
|
// [TestCase()]
|
||||||
public async Task ErrorResponse_ShouldNot_ConfirmSubscription()
|
// public async Task ErrorResponse_ShouldNot_ConfirmSubscription()
|
||||||
{
|
// {
|
||||||
// arrange
|
// // arrange
|
||||||
var channel = "trade_btcusd";
|
// var channel = "trade_btcusd";
|
||||||
var client = new TestSocketClient(opt =>
|
// var client = new TestSocketClient(opt =>
|
||||||
{
|
// {
|
||||||
opt.OutputOriginalData = true;
|
// opt.OutputOriginalData = true;
|
||||||
opt.SocketSubscriptionsCombineTarget = 1;
|
// opt.SocketSubscriptionsCombineTarget = 1;
|
||||||
});
|
// });
|
||||||
var socket = client.CreateSocket();
|
// var socket = client.CreateSocket();
|
||||||
socket.CanConnect = true;
|
// socket.CanConnect = true;
|
||||||
client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, "https://test.test"));
|
// client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
|
||||||
|
|
||||||
// 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
|
||||||
ClassicAssert.IsFalse(client.SubClient.TestSubscription.Confirmed);
|
// ClassicAssert.IsTrue(client.SubClient.TestSubscription.Status != SubscriptionStatus.Subscribed);
|
||||||
}
|
// }
|
||||||
|
|
||||||
[TestCase()]
|
// [TestCase()]
|
||||||
public async Task SuccessResponse_Should_ConfirmSubscription()
|
// public async Task SuccessResponse_Should_ConfirmSubscription()
|
||||||
{
|
// {
|
||||||
// arrange
|
// // arrange
|
||||||
var channel = "trade_btcusd";
|
// var channel = "trade_btcusd";
|
||||||
var client = new TestSocketClient(opt =>
|
// var client = new TestSocketClient(opt =>
|
||||||
{
|
// {
|
||||||
opt.OutputOriginalData = true;
|
// opt.OutputOriginalData = true;
|
||||||
opt.SocketSubscriptionsCombineTarget = 1;
|
// opt.SocketSubscriptionsCombineTarget = 1;
|
||||||
});
|
// });
|
||||||
var socket = client.CreateSocket();
|
// var socket = client.CreateSocket();
|
||||||
socket.CanConnect = true;
|
// socket.CanConnect = true;
|
||||||
client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, "https://test.test"));
|
// client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
|
||||||
|
|
||||||
// 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
|
||||||
Assert.That(client.SubClient.TestSubscription.Confirmed);
|
// Assert.That(client.SubClient.TestSubscription.Status == SubscriptionStatus.Subscribed);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ using System.Text.Json;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using System;
|
using System;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using NUnit.Framework.Legacy;
|
|
||||||
using CryptoExchange.Net.Converters;
|
using CryptoExchange.Net.Converters;
|
||||||
using CryptoExchange.Net.Testing.Comparers;
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
@@ -146,7 +145,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 +170,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 +193,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 +212,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 +222,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 +268,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 +297,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 +353,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 +388,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 +412,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 +0,0 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
|
||||||
using CryptoExchange.Net.Sockets;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
|
||||||
{
|
|
||||||
internal class SubResponse
|
|
||||||
{
|
|
||||||
|
|
||||||
[JsonProperty("action")]
|
|
||||||
public string Action { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonProperty("channel")]
|
|
||||||
public string Channel { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonProperty("status")]
|
|
||||||
public string Status { get; set; } = null!;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class UnsubResponse
|
|
||||||
{
|
|
||||||
[JsonProperty("action")]
|
|
||||||
public string Action { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonProperty("status")]
|
|
||||||
public string Status { get; set; } = null!;
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
ListenerIdentifiers = new HashSet<string> { request + "-" + channel };
|
|
||||||
}
|
|
||||||
|
|
||||||
public override CallResult<SubResponse> HandleMessage(SocketConnection connection, DataEvent<SubResponse> message)
|
|
||||||
{
|
|
||||||
if (!message.Data.Status.Equals("confirmed", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return new CallResult<SubResponse>(new ServerError(message.Data.Status));
|
|
||||||
}
|
|
||||||
|
|
||||||
return base.HandleMessage(connection, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
using CryptoExchange.Net.Sockets;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
ListenerIdentifiers = new HashSet<string> { identifier };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
|
||||||
using CryptoExchange.Net.Sockets;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
|
||||||
{
|
|
||||||
internal class TestSubscription<T> : Subscription<object, object>
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
_handler = handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override CallResult DoHandleMessage(SocketConnection connection, DataEvent<object> message)
|
|
||||||
{
|
|
||||||
var data = (T)message.Data;
|
|
||||||
_handler.Invoke(message.As(data));
|
|
||||||
return new CallResult(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Type GetMessageType(IMessageAccessor message) => typeof(T);
|
|
||||||
public override Query GetSubQuery(SocketConnection connection) => new TestQuery("sub", new object(), false, 1);
|
|
||||||
public override Query GetUnsubQuery() => new TestQuery("unsub", new object(), false, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-38
@@ -1,38 +0,0 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
|
||||||
using CryptoExchange.Net.Sockets;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Moq;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
|
||||||
{
|
|
||||||
internal class TestSubscriptionWithResponseCheck<T> : Subscription<SubResponse, UnsubResponse>
|
|
||||||
{
|
|
||||||
private readonly Action<DataEvent<T>> _handler;
|
|
||||||
private readonly string _channel;
|
|
||||||
|
|
||||||
public override HashSet<string> ListenerIdentifiers { get; set; }
|
|
||||||
|
|
||||||
public TestSubscriptionWithResponseCheck(string channel, Action<DataEvent<T>> handler) : base(Mock.Of<ILogger>(), false)
|
|
||||||
{
|
|
||||||
ListenerIdentifiers = new HashSet<string>() { channel };
|
|
||||||
_handler = handler;
|
|
||||||
_channel = channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override CallResult DoHandleMessage(SocketConnection connection, DataEvent<object> message)
|
|
||||||
{
|
|
||||||
var data = (T)message.Data;
|
|
||||||
_handler.Invoke(message.As(data));
|
|
||||||
return new CallResult(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Type GetMessageType(IMessageAccessor message) => typeof(T);
|
|
||||||
public override Query GetSubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "subscribe", false, 1);
|
|
||||||
public override Query GetUnsubQuery() => new TestChannelQuery(_channel, "unsubscribe", false, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
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.MessageParsing.DynamicConverters;
|
||||||
|
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 Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
@@ -21,12 +23,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()));
|
||||||
}
|
}
|
||||||
@@ -39,6 +43,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
public class TestSubClient : RestApiClient
|
public class TestSubClient : RestApiClient
|
||||||
{
|
{
|
||||||
|
protected override IRestMessageHandler MessageHandler => throw new NotImplementedException();
|
||||||
|
|
||||||
public TestSubClient(RestExchangeOptions<TestEnvironment> options, RestApiOptions apiOptions) : base(new TraceLogger(), null, "https://localhost:123", options, apiOptions)
|
public TestSubClient(RestExchangeOptions<TestEnvironment> options, RestApiOptions apiOptions) : base(new TraceLogger(), null, "https://localhost:123", options, apiOptions)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -49,7 +55,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;
|
||||||
@@ -57,23 +63,35 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
/// <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 override TimeSpan? GetTimeOffset() => null;
|
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions());
|
||||||
public override TimeSyncInfo GetTimeSyncInfo() => null;
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestAuthProvider : AuthenticationProvider
|
public class TestAuthProvider : AuthenticationProvider
|
||||||
{
|
{
|
||||||
|
public override ApiCredentialsType[] SupportedCredentialTypes => [ApiCredentialsType.Hmac];
|
||||||
|
|
||||||
public TestAuthProvider(ApiCredentials credentials) : base(credentials)
|
public TestAuthProvider(ApiCredentials credentials) : base(credentials)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class TestEnvironment : TradeEnvironment
|
||||||
|
{
|
||||||
|
public string TestAddress { get; }
|
||||||
|
|
||||||
|
public TestEnvironment(string name, string url) : base(name)
|
||||||
|
{
|
||||||
|
TestAddress = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,15 @@ 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 Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Linq;
|
||||||
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||||
{
|
{
|
||||||
@@ -47,19 +50,19 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
var response = new Mock<IResponse>();
|
var response = new Mock<IResponse>();
|
||||||
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(It.IsAny<CancellationToken>())).Returns(Task.FromResult((Stream)responseStream));
|
||||||
|
|
||||||
var headers = new Dictionary<string, IEnumerable<string>>();
|
var headers = new HttpRequestMessage().Headers;
|
||||||
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);
|
||||||
|
|
||||||
var factory = Mock.Get(Api1.RequestFactory);
|
var factory = Mock.Get(Api1.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
|
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
|
||||||
{
|
{
|
||||||
request.Setup(a => a.Uri).Returns(uri);
|
request.Setup(a => a.Uri).Returns(uri);
|
||||||
request.Setup(a => a.Method).Returns(method);
|
request.Setup(a => a.Method).Returns(method);
|
||||||
@@ -67,8 +70,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
.Returns(request.Object);
|
.Returns(request.Object);
|
||||||
|
|
||||||
factory = Mock.Get(Api2.RequestFactory);
|
factory = Mock.Get(Api2.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
|
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
|
||||||
{
|
{
|
||||||
request.Setup(a => a.Uri).Returns(uri);
|
request.Setup(a => a.Uri).Returns(uri);
|
||||||
request.Setup(a => a.Method).Returns(method);
|
request.Setup(a => a.Method).Returns(method);
|
||||||
@@ -84,16 +87,16 @@ 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 HttpRequestMessage().Headers);
|
||||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
|
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
|
||||||
|
|
||||||
var factory = Mock.Get(Api1.RequestFactory);
|
var factory = Mock.Get(Api1.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Returns(request.Object);
|
.Returns(request.Object);
|
||||||
|
|
||||||
|
|
||||||
factory = Mock.Get(Api2.RequestFactory);
|
factory = Mock.Get(Api2.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Returns(request.Object);
|
.Returns(request.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,29 +109,31 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
var response = new Mock<IResponse>();
|
var response = new Mock<IResponse>();
|
||||||
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(It.IsAny<CancellationToken>())).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(new HttpRequestMessage().Headers);
|
||||||
|
|
||||||
var factory = Mock.Get(Api1.RequestFactory);
|
var factory = Mock.Get(Api1.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||||
.Returns(request.Object);
|
.Returns(request.Object);
|
||||||
|
|
||||||
factory = Mock.Get(Api2.RequestFactory);
|
factory = Mock.Get(Api2.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||||
.Returns(request.Object);
|
.Returns(request.Object);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestRestApi1Client : RestApiClient
|
public class TestRestApi1Client : RestApiClient
|
||||||
{
|
{
|
||||||
|
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
|
||||||
|
|
||||||
public TestRestApi1Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api1Options)
|
public TestRestApi1Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api1Options)
|
||||||
{
|
{
|
||||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||||
@@ -137,14 +142,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)
|
||||||
@@ -152,11 +160,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
ParameterPositions[method] = position;
|
ParameterPositions[method] = position;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override TimeSpan? GetTimeOffset()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||||
=> new TestAuthProvider(credentials);
|
=> new TestAuthProvider(credentials);
|
||||||
|
|
||||||
@@ -164,40 +167,28 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override TimeSyncInfo GetTimeSyncInfo()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestRestApi2Client : RestApiClient
|
public class TestRestApi2Client : RestApiClient
|
||||||
{
|
{
|
||||||
|
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
|
||||||
|
|
||||||
public TestRestApi2Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api2Options)
|
public TestRestApi2Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api2Options)
|
||||||
{
|
{
|
||||||
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)
|
|
||||||
{
|
|
||||||
var errorData = accessor.Deserialize<TestError>();
|
|
||||||
|
|
||||||
return new ServerError(errorData.Data.ErrorCode, errorData.Data.ErrorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override TimeSpan? GetTimeOffset()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||||
=> new TestAuthProvider(credentials);
|
=> new TestAuthProvider(credentials);
|
||||||
|
|
||||||
@@ -206,15 +197,13 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override TimeSyncInfo GetTimeSyncInfo()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||||
|
using CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Errors;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||||
|
{
|
||||||
|
internal class TestRestMessageHandler : JsonRestMessageHandler
|
||||||
|
{
|
||||||
|
private ErrorMapping _errorMapping = new ErrorMapping([]);
|
||||||
|
public override JsonSerializerOptions Options => new JsonSerializerOptions();
|
||||||
|
|
||||||
|
public override ValueTask<Error> ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream)
|
||||||
|
{
|
||||||
|
var errorData = JsonSerializer.Deserialize<TestError>(responseStream);
|
||||||
|
|
||||||
|
return new ValueTask<Error>(new ServerError(errorData.ErrorCode, _errorMapping.GetErrorInfo(errorData.ErrorCode.ToString(), errorData.ErrorMessage)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CryptoExchange.Net.Authentication;
|
|
||||||
using CryptoExchange.Net.Clients;
|
|
||||||
using CryptoExchange.Net.Converters.MessageParsing;
|
|
||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Options;
|
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
|
||||||
using CryptoExchange.Net.Sockets;
|
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Moq;
|
|
||||||
using CryptoExchange.Net.Testing.Implementations;
|
|
||||||
using CryptoExchange.Net.SharedApis;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|
||||||
{
|
|
||||||
internal class TestSocketClient: BaseSocketClient
|
|
||||||
{
|
|
||||||
public TestSubSocketClient SubClient { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a new instance of KucoinSocketClient
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="optionsFunc">Configure the options to use for this client</param>
|
|
||||||
public TestSocketClient(Action<TestSocketOptions> optionsDelegate = null)
|
|
||||||
: this(Options.Create(ApplyOptionsDelegate(optionsDelegate)), null)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public TestSocketClient(IOptions<TestSocketOptions> options, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
|
|
||||||
{
|
|
||||||
Initialize(options.Value);
|
|
||||||
|
|
||||||
SubClient = AddApiClient(new TestSubSocketClient(options.Value, options.Value.SubOptions));
|
|
||||||
SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object;
|
|
||||||
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public TestSocket CreateSocket()
|
|
||||||
{
|
|
||||||
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com"));
|
|
||||||
return (TestSocket)SubClient.CreateSocketInternal("https://localhost:123/");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TestEnvironment : TradeEnvironment
|
|
||||||
{
|
|
||||||
public string TestAddress { get; }
|
|
||||||
|
|
||||||
public TestEnvironment(string name, string url) : base(name)
|
|
||||||
{
|
|
||||||
TestAddress = url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TestSocketOptions: SocketExchangeOptions<TestEnvironment>
|
|
||||||
{
|
|
||||||
public static TestSocketOptions Default = new TestSocketOptions
|
|
||||||
{
|
|
||||||
Environment = new TestEnvironment("Live", "https://test.test")
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public TestSocketOptions()
|
|
||||||
{
|
|
||||||
Default?.Set(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SocketApiOptions SubOptions { get; set; } = new SocketApiOptions();
|
|
||||||
|
|
||||||
internal TestSocketOptions Set(TestSocketOptions targetOptions)
|
|
||||||
{
|
|
||||||
targetOptions = base.Set<TestSocketOptions>(targetOptions);
|
|
||||||
targetOptions.SubOptions = SubOptions.Set(targetOptions.SubOptions);
|
|
||||||
return targetOptions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TestSubSocketClient : SocketApiClient
|
|
||||||
{
|
|
||||||
private MessagePath _channelPath = MessagePath.Get().Property("channel");
|
|
||||||
private MessagePath _actionPath = MessagePath.Get().Property("action");
|
|
||||||
private MessagePath _topicPath = MessagePath.Get().Property("topic");
|
|
||||||
|
|
||||||
public Subscription TestSubscription { get; private set; } = null;
|
|
||||||
|
|
||||||
public TestSubSocketClient(TestSocketOptions options, SocketApiOptions apiOptions) : base(new TraceLogger(), options.Environment.TestAddress, options, apiOptions)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
|
||||||
|
|
||||||
internal IWebsocket CreateSocketInternal(string address)
|
|
||||||
{
|
|
||||||
return CreateSocket(address);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
|
||||||
=> new TestAuthProvider(credentials);
|
|
||||||
|
|
||||||
public CallResult ConnectSocketSub(SocketConnection sub)
|
|
||||||
{
|
|
||||||
return ConnectSocketAsync(sub).Result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string GetListenerIdentifier(IMessageAccessor message)
|
|
||||||
{
|
|
||||||
if (!message.IsJson)
|
|
||||||
{
|
|
||||||
return "topic";
|
|
||||||
}
|
|
||||||
|
|
||||||
var id = message.GetValue<string>(_channelPath);
|
|
||||||
id ??= message.GetValue<string>(_topicPath);
|
|
||||||
|
|
||||||
return message.GetValue<string>(_actionPath) + "-" + id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<CallResult<UpdateSubscription>> SubscribeToSomethingAsync(string channel, Action<DataEvent<string>> onUpdate, CancellationToken ct)
|
|
||||||
{
|
|
||||||
TestSubscription = new TestSubscriptionWithResponseCheck<string>(channel, onUpdate);
|
|
||||||
return SubscribeAsync(TestSubscription, ct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests
|
||||||
|
{
|
||||||
|
[JsonSerializable(typeof(string))]
|
||||||
|
[JsonSerializable(typeof(int))]
|
||||||
|
[JsonSerializable(typeof(Dictionary<string, string>))]
|
||||||
|
[JsonSerializable(typeof(IDictionary<string, string>))]
|
||||||
|
[JsonSerializable(typeof(Dictionary<string, object>))]
|
||||||
|
[JsonSerializable(typeof(IDictionary<string, object>))]
|
||||||
|
[JsonSerializable(typeof(TestObject))]
|
||||||
|
internal partial class TestSerializerContext : JsonSerializerContext
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Converters.MessageParsing;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Authentication
|
namespace CryptoExchange.Net.Authentication
|
||||||
{
|
{
|
||||||
@@ -20,6 +19,11 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string Secret { get; set; }
|
public string Secret { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The api passphrase. Not needed on all exchanges
|
||||||
|
/// </summary>
|
||||||
|
public string? Pass { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Type of the credentials
|
/// Type of the credentials
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -30,8 +34,9 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key">The api key / label used for identification</param>
|
/// <param name="key">The api key / label used for identification</param>
|
||||||
/// <param name="secret">The api secret or private key used for signing</param>
|
/// <param name="secret">The api secret or private key used for signing</param>
|
||||||
|
/// <param name="pass">The api pass for the key. Not always needed</param>
|
||||||
/// <param name="credentialType">The type of credentials</param>
|
/// <param name="credentialType">The type of credentials</param>
|
||||||
public ApiCredentials(string key, string secret, ApiCredentialsType credentialType = ApiCredentialsType.Hmac)
|
public ApiCredentials(string key, string secret, string? pass = null, ApiCredentialsType credentialType = ApiCredentialsType.Hmac)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
|
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
|
||||||
throw new ArgumentException("Key and secret can't be null/empty");
|
throw new ArgumentException("Key and secret can't be null/empty");
|
||||||
@@ -39,6 +44,49 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
CredentialType = credentialType;
|
CredentialType = credentialType;
|
||||||
Key = key;
|
Key = key;
|
||||||
Secret = secret;
|
Secret = secret;
|
||||||
|
Pass = pass;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create API credentials using an API key and secret generated by the server
|
||||||
|
/// </summary>
|
||||||
|
public static ApiCredentials HmacCredentials(string apiKey, string apiSecret, string? pass)
|
||||||
|
{
|
||||||
|
return new ApiCredentials(apiKey, apiSecret, pass, ApiCredentialsType.Hmac);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create API credentials using an API key and an RSA private key in PEM format
|
||||||
|
/// </summary>
|
||||||
|
public static ApiCredentials RsaPemCredentials(string apiKey, string privateKey)
|
||||||
|
{
|
||||||
|
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.RsaPem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create API credentials using an API key and an RSA private key in XML format
|
||||||
|
/// </summary>
|
||||||
|
public static ApiCredentials RsaXmlCredentials(string apiKey, string privateKey)
|
||||||
|
{
|
||||||
|
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.RsaXml);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create API credentials using an API key and an Ed25519 private key
|
||||||
|
/// </summary>
|
||||||
|
public static ApiCredentials Ed25519Credentials(string apiKey, string privateKey)
|
||||||
|
{
|
||||||
|
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.Ed25519);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load a key from a file
|
||||||
|
/// </summary>
|
||||||
|
public static string ReadFromFile(string path)
|
||||||
|
{
|
||||||
|
using var fileStream = File.OpenRead(path);
|
||||||
|
using var streamReader = new StreamReader(fileStream);
|
||||||
|
return streamReader.ReadToEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -47,28 +95,7 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual ApiCredentials Copy()
|
public virtual ApiCredentials Copy()
|
||||||
{
|
{
|
||||||
return new ApiCredentials(Key, Secret, CredentialType);
|
return new ApiCredentials(Key, Secret, Pass, CredentialType);
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create Api credentials providing a stream containing json data. The json data should include two values: apiKey and apiSecret
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="inputStream">The stream containing the json data</param>
|
|
||||||
/// <param name="identifierKey">A key to identify the credentials for the API. For example, when set to `binanceKey` the json data should contain a value for the property `binanceKey`. Defaults to 'apiKey'.</param>
|
|
||||||
/// <param name="identifierSecret">A key to identify the credentials for the API. For example, when set to `binanceSecret` the json data should contain a value for the property `binanceSecret`. Defaults to 'apiSecret'.</param>
|
|
||||||
public static ApiCredentials FromStream(Stream inputStream, string? identifierKey = null, string? identifierSecret = null)
|
|
||||||
{
|
|
||||||
var accessor = new SystemTextJsonStreamMessageAccessor();
|
|
||||||
if (!accessor.Read(inputStream, false).Result)
|
|
||||||
throw new ArgumentException("Input stream not valid json data");
|
|
||||||
|
|
||||||
var key = accessor.GetValue<string>(MessagePath.Get().Property(identifierKey ?? "apiKey"));
|
|
||||||
var secret = accessor.GetValue<string>(MessagePath.Get().Property(identifierSecret ?? "apiSecret"));
|
|
||||||
if (key == null || secret == null)
|
|
||||||
throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");
|
|
||||||
|
|
||||||
inputStream.Seek(0, SeekOrigin.Begin);
|
|
||||||
return new ApiCredentials(key, secret);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
/// <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 pem/base64 format. Only available for .NetStandard 2.1 and up, use xml format for lower.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
RsaPem
|
RsaPem,
|
||||||
|
/// <summary>
|
||||||
|
/// Ed25519 keys credentials
|
||||||
|
/// </summary>
|
||||||
|
Ed25519
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,17 @@
|
|||||||
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;
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
using NSec.Cryptography;
|
||||||
|
#endif
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using CryptoExchange.Net.Sockets;
|
||||||
|
using CryptoExchange.Net.Sockets.Default;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Authentication
|
namespace CryptoExchange.Net.Authentication
|
||||||
{
|
{
|
||||||
@@ -18,6 +23,11 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
{
|
{
|
||||||
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
|
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The supported credential types
|
||||||
|
/// </summary>
|
||||||
|
public abstract ApiCredentialsType[] SupportedCredentialTypes { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provided credentials
|
/// Provided credentials
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -28,10 +38,21 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected byte[] _sBytes;
|
protected byte[] _sBytes;
|
||||||
|
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
/// <summary>
|
||||||
|
/// The Ed25519 private key
|
||||||
|
/// </summary>
|
||||||
|
protected Key? Ed25519Key;
|
||||||
|
#endif
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the API key of the current credentials
|
/// Get the API key of the current credentials
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ApiKey => _credentials.Key!;
|
public string ApiKey => _credentials.Key!;
|
||||||
|
/// <summary>
|
||||||
|
/// Get the Passphrase of the current credentials
|
||||||
|
/// </summary>
|
||||||
|
public string? Pass => _credentials.Pass;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@@ -42,35 +63,34 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
if (credentials.Key == null || credentials.Secret == null)
|
if (credentials.Key == null || credentials.Secret == null)
|
||||||
throw new ArgumentException("ApiKey/Secret needed");
|
throw new ArgumentException("ApiKey/Secret needed");
|
||||||
|
|
||||||
|
if (!SupportedCredentialTypes.Any(x => x == credentials.CredentialType))
|
||||||
|
throw new ArgumentException($"Credential type {credentials.CredentialType} not supported");
|
||||||
|
|
||||||
|
if (credentials.CredentialType == ApiCredentialsType.Ed25519)
|
||||||
|
{
|
||||||
|
#if !NET8_0_OR_GREATER
|
||||||
|
throw new ArgumentException($"Credential type Ed25519 only supported on Net8.0 or newer");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
_credentials = credentials;
|
_credentials = credentials;
|
||||||
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret);
|
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Authenticate a request. Output parameters should include the providedParameters input
|
/// Authenticate a REST request
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="apiClient">The Api client sending the request</param>
|
/// <param name="apiClient">The API client sending the request</param>
|
||||||
/// <param name="uri">The uri for the request</param>
|
/// <param name="requestConfig">The request configuration</param>
|
||||||
/// <param name="method">The method of the request</param>
|
public abstract void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig);
|
||||||
/// <param name="auth">If the requests should be authenticated</param>
|
|
||||||
/// <param name="arraySerialization">Array serialization type</param>
|
/// <summary>
|
||||||
/// <param name="requestBodyFormat">The formatting of the request body</param>
|
/// Get an authentication query for a websocket
|
||||||
/// <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>
|
/// </summary>
|
||||||
/// <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="apiClient">The API client sending the request</param>
|
||||||
/// <param name="headers">The headers that should be send with the request</param>
|
/// <param name="connection">The connection to authenticate</param>
|
||||||
/// <param name="parameterPosition">The position where the providedParameters should go</param>
|
/// <param name="context">Optional context required for creating the authentication query</param>
|
||||||
public abstract void AuthenticateRequest(
|
public virtual Query? GetAuthenticationQuery(SocketApiClient apiClient, SocketConnection connection, Dictionary<string, object?>? context = null) => null;
|
||||||
RestApiClient apiClient,
|
|
||||||
Uri uri,
|
|
||||||
HttpMethod method,
|
|
||||||
ref IDictionary<string, object>? uriParameters,
|
|
||||||
ref IDictionary<string, object>? bodyParameters,
|
|
||||||
ref Dictionary<string, string>? headers,
|
|
||||||
bool auth,
|
|
||||||
ArrayParametersSerialization arraySerialization,
|
|
||||||
HttpMethodParameterPosition parameterPosition,
|
|
||||||
RequestBodyFormat requestBodyFormat
|
|
||||||
);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// SHA256 sign the data and return the bytes
|
/// SHA256 sign the data and return the bytes
|
||||||
@@ -364,6 +384,36 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ed25519 sign the data
|
||||||
|
/// </summary>
|
||||||
|
public string SignEd25519(string data, SignOutputType? outputType = null)
|
||||||
|
=> SignEd25519(Encoding.ASCII.GetBytes(data), outputType);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ed25519 sign the data
|
||||||
|
/// </summary>
|
||||||
|
public string SignEd25519(byte[] data, SignOutputType? outputType = null)
|
||||||
|
{
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
if (Ed25519Key == null)
|
||||||
|
{
|
||||||
|
var key = _credentials.Secret!
|
||||||
|
.Replace("\n", "")
|
||||||
|
.Replace("-----BEGIN PRIVATE KEY-----", "")
|
||||||
|
.Replace("-----END PRIVATE KEY-----", "")
|
||||||
|
.Trim();
|
||||||
|
var keyBytes = Convert.FromBase64String(key);
|
||||||
|
Ed25519Key = Key.Import(SignatureAlgorithm.Ed25519, keyBytes, KeyBlobFormat.PkixPrivateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
var resultBytes = SignatureAlgorithm.Ed25519.Sign(Ed25519Key, data);
|
||||||
|
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||||
|
#else
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
private RSA CreateRSA()
|
private RSA CreateRSA()
|
||||||
{
|
{
|
||||||
var rsa = RSA.Create();
|
var rsa = RSA.Create();
|
||||||
@@ -402,6 +452,14 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <param name="buff"></param>
|
/// <param name="buff"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected static string BytesToHexString(byte[] buff)
|
protected static string BytesToHexString(byte[] buff)
|
||||||
|
=> BytesToHexString(new ArraySegment<byte>(buff));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert byte array to hex string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="buff"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected static string BytesToHexString(ArraySegment<byte> buff)
|
||||||
{
|
{
|
||||||
#if NET9_0_OR_GREATER
|
#if NET9_0_OR_GREATER
|
||||||
return Convert.ToHexString(buff);
|
return Convert.ToHexString(buff);
|
||||||
@@ -413,6 +471,26 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a hex encoded string to byte array
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hexString"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected static byte[] HexToBytesString(string hexString)
|
||||||
|
{
|
||||||
|
if (hexString.StartsWith("0x"))
|
||||||
|
hexString = hexString.Substring(2);
|
||||||
|
|
||||||
|
byte[] bytes = new byte[hexString.Length / 2];
|
||||||
|
for (int i = 0; i < hexString.Length; i += 2)
|
||||||
|
{
|
||||||
|
string hexSubstring = hexString.Substring(i, 2);
|
||||||
|
bytes[i / 2] = Convert.ToByte(hexSubstring, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert byte array to base64 string
|
/// Convert byte array to base64 string
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -426,32 +504,50 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get current timestamp including the time sync offset from the api client
|
/// Get current timestamp including the time sync offset from the api client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="apiClient"></param>
|
protected DateTime GetTimestamp(RestApiClient apiClient, bool includeOneSecondOffset = true)
|
||||||
/// <returns></returns>
|
|
||||||
protected DateTime GetTimestamp(RestApiClient apiClient)
|
|
||||||
{
|
{
|
||||||
return TimeProvider.GetTime().Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!;
|
var result = TimeProvider.GetTime().Add(TimeOffsetManager.GetRestOffset(apiClient.ClientName) ?? TimeSpan.Zero)!;
|
||||||
|
if (includeOneSecondOffset)
|
||||||
|
result = result.AddSeconds(-1);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get current timestamp including the time sync offset from the api client
|
||||||
|
/// </summary>
|
||||||
|
protected DateTime GetTimestamp(SocketApiClient apiClient, bool includeOneSecondOffset = true)
|
||||||
|
{
|
||||||
|
var result = TimeProvider.GetTime().Add(TimeOffsetManager.GetSocketOffset(apiClient.ClientName) ?? TimeSpan.Zero)!;
|
||||||
|
if (includeOneSecondOffset)
|
||||||
|
result = result.AddSeconds(-1);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get millisecond timestamp as a string including the time sync offset from the api client
|
/// Get millisecond timestamp as a string including the time sync offset from the api client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="apiClient"></param>
|
protected string GetMillisecondTimestamp(RestApiClient apiClient, bool includeOneSecondOffset = true)
|
||||||
/// <returns></returns>
|
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).Value.ToString(CultureInfo.InvariantCulture);
|
||||||
protected string GetMillisecondTimestamp(RestApiClient apiClient)
|
|
||||||
{
|
/// <summary>
|
||||||
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
|
/// Get millisecond timestamp as a string including the time sync offset from the api client
|
||||||
}
|
/// </summary>
|
||||||
|
protected string GetMillisecondTimestamp(SocketApiClient apiClient, bool includeOneSecondOffset = true)
|
||||||
|
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).Value.ToString(CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get millisecond timestamp as a long including the time sync offset from the api client
|
/// Get millisecond timestamp as a long including the time sync offset from the api client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="apiClient"></param>
|
protected long GetMillisecondTimestampLong(RestApiClient apiClient, bool includeOneSecondOffset = true)
|
||||||
/// <returns></returns>
|
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).Value;
|
||||||
protected long GetMillisecondTimestampLong(RestApiClient apiClient)
|
|
||||||
{
|
/// <summary>
|
||||||
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value;
|
/// Get millisecond timestamp as a long including the time sync offset from the api client
|
||||||
}
|
/// </summary>
|
||||||
|
protected long GetMillisecondTimestampLong(SocketApiClient apiClient, bool includeOneSecondOffset = true)
|
||||||
|
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).Value;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Return the serialized request body
|
/// Return the serialized request body
|
||||||
@@ -461,10 +557,13 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected static string GetSerializedBody(IMessageSerializer serializer, IDictionary<string, object> parameters)
|
protected static string GetSerializedBody(IMessageSerializer serializer, IDictionary<string, object> parameters)
|
||||||
{
|
{
|
||||||
if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
|
if (serializer is not IStringMessageSerializer stringSerializer)
|
||||||
return serializer.Serialize(value);
|
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
|
else
|
||||||
return serializer.Serialize(parameters);
|
return stringSerializer.Serialize(parameters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Caching
|
namespace CryptoExchange.Net.Caching
|
||||||
{
|
{
|
||||||
internal class MemoryCache
|
internal class MemoryCache
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
|
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
private readonly Lock _lock = new Lock();
|
||||||
|
#else
|
||||||
|
private readonly object _lock = new object();
|
||||||
|
#endif
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a new cache entry. Will override an existing entry if it already exists
|
/// Add a new cache entry. Will override an existing entry if it already exists
|
||||||
@@ -26,16 +33,13 @@ namespace CryptoExchange.Net.Caching
|
|||||||
/// <returns>Cached value if it was in cache</returns>
|
/// <returns>Cached value if it was in cache</returns>
|
||||||
public object? Get(string key, TimeSpan maxAge)
|
public object? Get(string key, TimeSpan maxAge)
|
||||||
{
|
{
|
||||||
|
foreach (var item in _cache.Where(x => DateTime.UtcNow - x.Value.CacheTime > maxAge).ToList())
|
||||||
|
_cache.TryRemove(item.Key, out _);
|
||||||
|
|
||||||
_cache.TryGetValue(key, out CacheItem? value);
|
_cache.TryGetValue(key, out CacheItem? value);
|
||||||
if (value == null)
|
if (value == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (DateTime.UtcNow - value.CacheTime > maxAge)
|
|
||||||
{
|
|
||||||
_cache.TryRemove(key, out _);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.Value;
|
return value.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces.Clients;
|
||||||
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;
|
||||||
@@ -13,6 +13,8 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class BaseApiClient : IDisposable, IBaseApiClient
|
public abstract class BaseApiClient : IDisposable, IBaseApiClient
|
||||||
{
|
{
|
||||||
|
private string? _clientName;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Logger
|
/// Logger
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -23,6 +25,21 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected bool _disposing;
|
protected bool _disposing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Name of the client
|
||||||
|
/// </summary>
|
||||||
|
protected internal string ClientName
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_clientName != null)
|
||||||
|
return _clientName;
|
||||||
|
|
||||||
|
_clientName = GetType().Name;
|
||||||
|
return _clientName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The authentication provider for this API client. (null if no credentials are set)
|
/// The authentication provider for this API client. (null if no credentials are set)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -39,7 +56,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
public bool OutputOriginalData { get; }
|
public bool OutputOriginalData { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Authenticated => ApiOptions.ApiCredentials != null || ClientOptions.ApiCredentials != null;
|
public bool Authenticated => ApiCredentials != null;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ApiCredentials? ApiCredentials { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Api options
|
/// Api options
|
||||||
@@ -51,6 +71,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ExchangeOptions ClientOptions { get; }
|
public ExchangeOptions ClientOptions { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mapping of a response code to known error types
|
||||||
|
/// </summary>
|
||||||
|
protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -68,9 +93,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
ApiOptions = apiOptions;
|
ApiOptions = apiOptions;
|
||||||
OutputOriginalData = outputOriginalData;
|
OutputOriginalData = outputOriginalData;
|
||||||
BaseAddress = baseAddress;
|
BaseAddress = baseAddress;
|
||||||
|
ApiCredentials = apiCredentials?.Copy();
|
||||||
|
|
||||||
if (apiCredentials != null)
|
if (ApiCredentials != null)
|
||||||
AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy());
|
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -83,12 +109,22 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
|
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get error info for a response code
|
||||||
|
/// </summary>
|
||||||
|
public ErrorInfo GetErrorInfo(int code, string? message = null) => GetErrorInfo(code.ToString(), message);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get error info for a response code
|
||||||
|
/// </summary>
|
||||||
|
public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||||
{
|
{
|
||||||
ApiOptions.ApiCredentials = credentials;
|
ApiCredentials = credentials?.Copy();
|
||||||
if (credentials != null)
|
if (ApiCredentials != null)
|
||||||
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
|
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -97,9 +133,9 @@ namespace CryptoExchange.Net.Clients
|
|||||||
ClientOptions.Proxy = options.Proxy;
|
ClientOptions.Proxy = options.Proxy;
|
||||||
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
||||||
|
|
||||||
ApiOptions.ApiCredentials = options.ApiCredentials ?? ClientOptions.ApiCredentials;
|
ApiCredentials = options.ApiCredentials?.Copy() ?? ApiCredentials;
|
||||||
if (options.ApiCredentials != null)
|
if (ApiCredentials != null)
|
||||||
AuthenticationProvider = CreateAuthenticationProvider(options.ApiCredentials.Copy());
|
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
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;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net.Clients
|
||||||
{
|
{
|
||||||
@@ -49,7 +49,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected internal ILogger _logger;
|
protected internal ILogger _logger;
|
||||||
|
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
private readonly Lock _versionLock = new Lock();
|
||||||
|
#else
|
||||||
private readonly object _versionLock = new object();
|
private readonly object _versionLock = new object();
|
||||||
|
#endif
|
||||||
private Version _exchangeVersion;
|
private Version _exchangeVersion;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -66,8 +70,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
protected BaseClient(ILoggerFactory? logger, string exchange)
|
protected BaseClient(ILoggerFactory? logger, string exchange)
|
||||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||||
{
|
{
|
||||||
_logger = logger?.CreateLogger(exchange) ?? NullLoggerFactory.Instance.CreateLogger(exchange);
|
|
||||||
|
|
||||||
Exchange = exchange;
|
Exchange = exchange;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +106,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (ClientOptions == null)
|
if (ClientOptions == null)
|
||||||
throw new InvalidOperationException("Client should have called Initialize before adding API clients");
|
throw new InvalidOperationException("Client should have called Initialize before adding API clients");
|
||||||
|
|
||||||
_logger.Log(LogLevel.Trace, $" {apiClient.GetType().Name}, base address: {apiClient.BaseAddress}");
|
|
||||||
ApiClients.Add(apiClient);
|
ApiClients.Add(apiClient);
|
||||||
return apiClient;
|
return apiClient;
|
||||||
}
|
}
|
||||||
@@ -124,7 +125,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void Dispose()
|
public virtual void Dispose()
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Debug, "Disposing client");
|
|
||||||
foreach (var client in ApiClients)
|
foreach (var client in ApiClients)
|
||||||
client.Dispose();
|
client.Dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces.Clients;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net.Clients
|
||||||
{
|
{
|
||||||
@@ -19,6 +20,9 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="name">The name of the API this client is for</param>
|
/// <param name="name">The name of the API this client is for</param>
|
||||||
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
||||||
{
|
{
|
||||||
|
_logger = loggerFactory?.CreateLogger(name + ".RestClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
|
||||||
|
|
||||||
|
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
using System;
|
using CryptoExchange.Net.Interfaces.Clients;
|
||||||
|
using CryptoExchange.Net.Logging.Extensions;
|
||||||
|
using CryptoExchange.Net.Objects.Options;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using CryptoExchange.Net.Logging.Extensions;
|
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net.Clients
|
||||||
{
|
{
|
||||||
@@ -28,15 +30,21 @@ namespace CryptoExchange.Net.Clients
|
|||||||
public int CurrentSubscriptions => ApiClients.OfType<SocketApiClient>().Sum(s => s.CurrentSubscriptions);
|
public int CurrentSubscriptions => ApiClients.OfType<SocketApiClient>().Sum(s => s.CurrentSubscriptions);
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public double IncomingKbps => ApiClients.OfType<SocketApiClient>().Sum(s => s.IncomingKbps);
|
public double IncomingKbps => ApiClients.OfType<SocketApiClient>().Sum(s => s.IncomingKbps);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public new SocketExchangeOptions ClientOptions => (SocketExchangeOptions)base.ClientOptions;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">Logger</param>
|
/// <param name="loggerFactory">Logger factory</param>
|
||||||
/// <param name="exchange">The name of the exchange this client is for</param>
|
/// <param name="name">The name of the exchange this client is for</param>
|
||||||
protected BaseSocketClient(ILoggerFactory? logger, string exchange) : base(logger, exchange)
|
protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
||||||
{
|
{
|
||||||
|
_logger = loggerFactory?.CreateLogger(name + ".SocketClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
|
||||||
|
|
||||||
|
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces.Clients;
|
||||||
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
|
||||||
{
|
{
|
||||||
@@ -24,24 +20,5 @@ namespace CryptoExchange.Net.Clients
|
|||||||
public CryptoRestClient(IServiceProvider serviceProvider) : base(serviceProvider)
|
public CryptoRestClient(IServiceProvider serviceProvider) : base(serviceProvider)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get a list of the registered ISpotClient implementations
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public IEnumerable<ISpotClient> GetSpotClients()
|
|
||||||
{
|
|
||||||
if (_serviceProvider == null)
|
|
||||||
return new List<ISpotClient>();
|
|
||||||
|
|
||||||
return _serviceProvider.GetServices<ISpotClient>().ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get an ISpotClient implementation by exchange name
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="exchangeName"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public ISpotClient? SpotClient(string exchangeName) => _serviceProvider?.GetServices<ISpotClient>()?.SingleOrDefault(s => s.ExchangeName.Equals(exchangeName, StringComparison.InvariantCultureIgnoreCase));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces.Clients;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net.Clients
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,19 @@
|
|||||||
using CryptoExchange.Net.Converters.JsonNet;
|
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Interfaces.Clients;
|
||||||
using CryptoExchange.Net.Logging.Extensions;
|
using CryptoExchange.Net.Logging.Extensions;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Errors;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
using CryptoExchange.Net.RateLimiting;
|
||||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
|
using CryptoExchange.Net.Sockets.Default;
|
||||||
|
using CryptoExchange.Net.Sockets.Default.Interfaces;
|
||||||
|
using CryptoExchange.Net.Sockets.HighPerf;
|
||||||
|
using CryptoExchange.Net.Sockets.HighPerf.Interfaces;
|
||||||
|
using CryptoExchange.Net.Sockets.Interfaces;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
@@ -24,13 +32,22 @@ namespace CryptoExchange.Net.Clients
|
|||||||
public abstract class SocketApiClient : BaseApiClient, ISocketApiClient
|
public abstract class SocketApiClient : BaseApiClient, ISocketApiClient
|
||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory();
|
public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IHighPerfConnectionFactory? HighPerfConnectionFactory { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// List of socket connections currently connecting/connected
|
/// List of socket connections currently connecting/connected
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected internal ConcurrentDictionary<int, SocketConnection> socketConnections = new();
|
protected internal ConcurrentDictionary<int, SocketConnection> _socketConnections = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of HighPerf socket connections currently connecting/connected
|
||||||
|
/// </summary>
|
||||||
|
protected internal ConcurrentDictionary<int, HighPerfSocketConnection> _highPerfSocketConnections = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Semaphore used while creating sockets
|
/// Semaphore used while creating sockets
|
||||||
@@ -42,6 +59,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10);
|
protected TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Keep alive timeout for websocket connection
|
||||||
|
/// </summary>
|
||||||
|
protected TimeSpan KeepAliveTimeout { get; set; } = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handlers for data from the socket which doesn't need to be forwarded to the caller. Ping or welcome messages for example.
|
/// Handlers for data from the socket which doesn't need to be forwarded to the caller. Ping or welcome messages for example.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -66,7 +88,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// Periodic task registrations
|
/// Periodic task registrations
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected List<PeriodicTaskRegistration> PeriodicTaskRegistrations { get; set; } = new List<PeriodicTaskRegistration>();
|
protected List<PeriodicTaskRegistration> PeriodicTaskRegistrations { get; set; } = new List<PeriodicTaskRegistration>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// List of address to keep an alive connection to
|
/// List of address to keep an alive connection to
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -77,30 +99,35 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected bool AllowTopicsOnTheSameConnection { get; set; } = true;
|
protected bool AllowTopicsOnTheSameConnection { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to continue processing and forward unparsable messages to handlers
|
||||||
|
/// </summary>
|
||||||
|
protected internal bool ProcessUnparsableMessages { get; set; } = false;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public double IncomingKbps
|
public double IncomingKbps
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (socketConnections.IsEmpty)
|
if (_socketConnections.IsEmpty)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
return socketConnections.Sum(s => s.Value.IncomingKbps);
|
return _socketConnections.Sum(s => s.Value.IncomingKbps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public int CurrentConnections => socketConnections.Count;
|
public int CurrentConnections => _socketConnections.Count;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public int CurrentSubscriptions
|
public int CurrentSubscriptions
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (socketConnections.IsEmpty)
|
if (_socketConnections.IsEmpty)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
return socketConnections.Sum(s => s.Value.UserSubscriptionCount);
|
return _socketConnections.Sum(s => s.Value.UserSubscriptionCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +137,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public new SocketApiOptions ApiOptions => (SocketApiOptions)base.ApiOptions;
|
public new SocketApiOptions ApiOptions => (SocketApiOptions)base.ApiOptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The max number of individual subscriptions on a single connection
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxIndividualSubscriptionsPerConnection { get; set; }
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -133,13 +165,13 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// Create a message accessor instance
|
/// Create a message accessor instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected internal virtual IByteMessageAccessor CreateAccessor() => new JsonNetByteMessageAccessor();
|
protected internal abstract IByteMessageAccessor CreateAccessor(WebSocketMessageType messageType);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a serializer instance
|
/// Create a serializer instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected internal virtual IMessageSerializer CreateSerializer() => new JsonNetMessageSerializer();
|
protected internal abstract IMessageSerializer CreateSerializer();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Keep an open connection to this url
|
/// Keep an open connection to this url
|
||||||
@@ -151,6 +183,24 @@ namespace CryptoExchange.Net.Clients
|
|||||||
DedicatedConnectionConfigs.Add(new DedicatedConnectionConfig() { SocketAddress = url, Authenticated = auth });
|
DedicatedConnectionConfigs.Add(new DedicatedConnectionConfig() { SocketAddress = url, Authenticated = auth });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update the timestamp offset between client and server based on the timestamp
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="timestamp">Timestamp received from the server</param>
|
||||||
|
public virtual void UpdateTimeOffset(DateTime timestamp)
|
||||||
|
{
|
||||||
|
if (timestamp == default)
|
||||||
|
return;
|
||||||
|
|
||||||
|
TimeOffsetManager.UpdateSocketOffset(ClientName, (DateTime.UtcNow - timestamp).TotalMilliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the time offset between client and server
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public virtual TimeSpan? GetTimeOffset() => TimeOffsetManager.GetSocketOffset(ClientName);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a query to periodically send on each connection
|
/// Add a query to periodically send on each connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -158,7 +208,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="interval"></param>
|
/// <param name="interval"></param>
|
||||||
/// <param name="queryDelegate"></param>
|
/// <param name="queryDelegate"></param>
|
||||||
/// <param name="callback"></param>
|
/// <param name="callback"></param>
|
||||||
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback)
|
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<ISocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback)
|
||||||
{
|
{
|
||||||
PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration
|
PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration
|
||||||
{
|
{
|
||||||
@@ -198,6 +248,9 @@ namespace CryptoExchange.Net.Clients
|
|||||||
return new CallResult<UpdateSubscription>(new NoApiCredentialsError());
|
return new CallResult<UpdateSubscription>(new NoApiCredentialsError());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (subscription.IndividualSubscriptionCount > MaxIndividualSubscriptionsPerConnection)
|
||||||
|
return new CallResult<UpdateSubscription>(ArgumentError.Invalid("subscriptions", $"Max number of subscriptions in a single call is {MaxIndividualSubscriptionsPerConnection}"));
|
||||||
|
|
||||||
SocketConnection socketConnection;
|
SocketConnection socketConnection;
|
||||||
var released = false;
|
var released = false;
|
||||||
// Wait for a semaphore here, so we only connect 1 socket at a time.
|
// Wait for a semaphore here, so we only connect 1 socket at a time.
|
||||||
@@ -206,9 +259,9 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
|
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException tce)
|
||||||
{
|
{
|
||||||
return new CallResult<UpdateSubscription>(new CancellationRequestedError());
|
return new CallResult<UpdateSubscription>(new CancellationRequestedError(tce));
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -216,7 +269,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
// Get a new or existing socket connection
|
// Get a new or existing socket connection
|
||||||
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, subscription.Topic).ConfigureAwait(false);
|
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic, subscription.IndividualSubscriptionCount).ConfigureAwait(false);
|
||||||
if (!socketResult)
|
if (!socketResult)
|
||||||
return socketResult.As<UpdateSubscription>(null);
|
return socketResult.As<UpdateSubscription>(null);
|
||||||
|
|
||||||
@@ -239,7 +292,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
|
|
||||||
var needsConnecting = !socketConnection.Connected;
|
var needsConnecting = !socketConnection.Connected;
|
||||||
|
|
||||||
var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated).ConfigureAwait(false);
|
var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated, ct).ConfigureAwait(false);
|
||||||
if (!connectResult)
|
if (!connectResult)
|
||||||
return new CallResult<UpdateSubscription>(connectResult.Error!);
|
return new CallResult<UpdateSubscription>(connectResult.Error!);
|
||||||
|
|
||||||
@@ -255,59 +308,164 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (socketConnection.PausedActivity)
|
if (socketConnection.PausedActivity)
|
||||||
{
|
{
|
||||||
_logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId);
|
_logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId);
|
||||||
return new CallResult<UpdateSubscription>(new ServerError("Socket is paused"));
|
return new CallResult<UpdateSubscription>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
|
||||||
}
|
}
|
||||||
|
|
||||||
var waitEvent = new AsyncResetEvent(false);
|
void HandleSubscriptionComplete(bool success, object? response)
|
||||||
var subQuery = subscription.GetSubQuery(socketConnection);
|
{
|
||||||
|
if (!success)
|
||||||
|
return;
|
||||||
|
|
||||||
|
subscription.HandleSubQueryResponse(socketConnection, response);
|
||||||
|
subscription.Status = SubscriptionStatus.Subscribed;
|
||||||
|
if (ct != default)
|
||||||
|
{
|
||||||
|
subscription.CancellationTokenRegistration = ct.Register(async () =>
|
||||||
|
{
|
||||||
|
_logger.CancellationTokenSetClosingSubscription(socketConnection.SocketId, subscription.Id);
|
||||||
|
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription.Status = SubscriptionStatus.Subscribing;
|
||||||
|
var subQuery = subscription.CreateSubscriptionQuery(socketConnection);
|
||||||
if (subQuery != null)
|
if (subQuery != null)
|
||||||
{
|
{
|
||||||
|
subQuery.OnComplete = () => HandleSubscriptionComplete(subQuery.Result?.Success ?? false, subQuery.Response);
|
||||||
|
|
||||||
// Send the request and wait for answer
|
// Send the request and wait for answer
|
||||||
var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, waitEvent).ConfigureAwait(false);
|
var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, ct).ConfigureAwait(false);
|
||||||
if (!subResult)
|
if (!subResult)
|
||||||
{
|
{
|
||||||
waitEvent?.Set();
|
|
||||||
var isTimeout = subResult.Error is CancellationRequestedError;
|
var isTimeout = subResult.Error is CancellationRequestedError;
|
||||||
if (isTimeout && subscription.Confirmed)
|
if (isTimeout && subscription.Status == SubscriptionStatus.Subscribed)
|
||||||
{
|
{
|
||||||
// No response received, but the subscription did receive updates. We'll assume success
|
// No response received, but the subscription did receive updates. We'll assume success
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
|
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
|
||||||
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
|
// If this was a server process error we still might need to send an unsubscribe to prevent messages coming in later
|
||||||
|
subscription.Status = SubscriptionStatus.Pending;
|
||||||
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
||||||
return new CallResult<UpdateSubscription>(subResult.Error!);
|
return new CallResult<UpdateSubscription>(subResult.Error!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
subscription.HandleSubQueryResponse(subQuery.Response!);
|
else
|
||||||
|
{
|
||||||
|
HandleSubscriptionComplete(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
|
||||||
|
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connect to an url and listen for data
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">The URL to connect to</param>
|
||||||
|
/// <param name="subscription">The subscription</param>
|
||||||
|
/// <param name="connectionFactory">The factory for creating a socket connection</param>
|
||||||
|
/// <param name="ct">Cancellation token for closing this subscription</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual async Task<CallResult<HighPerfUpdateSubscription>> SubscribeHighPerfAsync<TUpdateType>(
|
||||||
|
string url,
|
||||||
|
HighPerfSubscription<TUpdateType> subscription,
|
||||||
|
IHighPerfConnectionFactory connectionFactory,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (_disposing)
|
||||||
|
return new CallResult<HighPerfUpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
|
||||||
|
|
||||||
|
HighPerfSocketConnection<TUpdateType> socketConnection;
|
||||||
|
var released = false;
|
||||||
|
// Wait for a semaphore here, so we only connect 1 socket at a time.
|
||||||
|
// This is necessary for being able to see if connections can be combined
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException tce)
|
||||||
|
{
|
||||||
|
return new CallResult<HighPerfUpdateSubscription>(new CancellationRequestedError(tce));
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
// Get a new or existing socket connection
|
||||||
|
var socketResult = await GetHighPerfSocketConnection<TUpdateType>(url, connectionFactory, ct).ConfigureAwait(false);
|
||||||
|
if (!socketResult)
|
||||||
|
return socketResult.As<HighPerfUpdateSubscription>(null);
|
||||||
|
|
||||||
|
socketConnection = socketResult.Data;
|
||||||
|
|
||||||
|
// Add a subscription on the socket connection
|
||||||
|
var success = socketConnection.AddSubscription(subscription);
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
_logger.FailedToAddSubscriptionRetryOnDifferentConnection(socketConnection.SocketId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
|
||||||
|
{
|
||||||
|
// Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway
|
||||||
|
semaphoreSlim.Release();
|
||||||
|
released = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var needsConnecting = !socketConnection.Connected;
|
||||||
|
|
||||||
|
var connectResult = await ConnectIfNeededAsync(socketConnection, false, ct).ConfigureAwait(false);
|
||||||
|
if (!connectResult)
|
||||||
|
return new CallResult<HighPerfUpdateSubscription>(connectResult.Error!);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (!released)
|
||||||
|
semaphoreSlim.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
var subRequest = subscription.CreateSubscriptionQuery(socketConnection);
|
||||||
|
if (subRequest != null)
|
||||||
|
{
|
||||||
|
// Send the request and wait for answer
|
||||||
|
var sendResult = await socketConnection.SendAsync(subRequest).ConfigureAwait(false);
|
||||||
|
if (!sendResult)
|
||||||
|
{
|
||||||
|
await socketConnection.CloseAsync().ConfigureAwait(false);
|
||||||
|
return new CallResult<HighPerfUpdateSubscription>(sendResult.Error!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subscription.Confirmed = true;
|
|
||||||
if (ct != default)
|
if (ct != default)
|
||||||
{
|
{
|
||||||
subscription.CancellationTokenRegistration = ct.Register(async () =>
|
subscription.CancellationTokenRegistration = ct.Register(async () =>
|
||||||
{
|
{
|
||||||
_logger.CancellationTokenSetClosingSubscription(socketConnection.SocketId, subscription.Id);
|
_logger.CancellationTokenSetClosingSubscription(socketConnection.SocketId, subscription.Id);
|
||||||
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
await socketConnection.CloseAsync().ConfigureAwait(false);
|
||||||
}, false);
|
}, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
waitEvent?.Set();
|
|
||||||
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
|
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
|
||||||
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
|
return new CallResult<HighPerfUpdateSubscription>(new HighPerfUpdateSubscription(socketConnection, subscription));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send a query on a socket connection to the BaseAddress and wait for the response
|
/// Send a query on a socket connection to the BaseAddress and wait for the response
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="THandlerResponse">Expected result type</typeparam>
|
/// <typeparam name="THandlerResponse">Expected result type</typeparam>
|
||||||
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
|
|
||||||
/// <param name="query">The query</param>
|
/// <param name="query">The query</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual Task<CallResult<THandlerResponse>> QueryAsync<TServerResponse, THandlerResponse>(Query<TServerResponse, THandlerResponse> query, CancellationToken ct = default)
|
protected virtual Task<CallResult<THandlerResponse>> QueryAsync<THandlerResponse>(Query<THandlerResponse> query, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return QueryAsync(BaseAddress, query, ct);
|
return QueryAsync(BaseAddress, query, ct);
|
||||||
}
|
}
|
||||||
@@ -316,12 +474,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// Send a query on a socket connection and wait for the response
|
/// Send a query on a socket connection and wait for the response
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="THandlerResponse">Expected result type</typeparam>
|
/// <typeparam name="THandlerResponse">Expected result type</typeparam>
|
||||||
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
|
|
||||||
/// <param name="url">The url for the request</param>
|
/// <param name="url">The url for the request</param>
|
||||||
/// <param name="query">The query</param>
|
/// <param name="query">The query</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult<THandlerResponse>> QueryAsync<TServerResponse, THandlerResponse>(string url, Query<TServerResponse, THandlerResponse> query, CancellationToken ct = default)
|
protected virtual async Task<CallResult<THandlerResponse>> QueryAsync<THandlerResponse>(string url, Query<THandlerResponse> query, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (_disposing)
|
if (_disposing)
|
||||||
return new CallResult<THandlerResponse>(new InvalidOperationError("Client disposed, can't query"));
|
return new CallResult<THandlerResponse>(new InvalidOperationError("Client disposed, can't query"));
|
||||||
@@ -334,7 +491,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var socketResult = await GetSocketConnection(url, query.Authenticated, true).ConfigureAwait(false);
|
var socketResult = await GetSocketConnection(url, query.Authenticated, true, ct).ConfigureAwait(false);
|
||||||
if (!socketResult)
|
if (!socketResult)
|
||||||
return socketResult.As<THandlerResponse>(default);
|
return socketResult.As<THandlerResponse>(default);
|
||||||
|
|
||||||
@@ -347,7 +504,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
released = true;
|
released = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated).ConfigureAwait(false);
|
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated, ct).ConfigureAwait(false);
|
||||||
if (!connectResult)
|
if (!connectResult)
|
||||||
return new CallResult<THandlerResponse>(connectResult.Error!);
|
return new CallResult<THandlerResponse>(connectResult.Error!);
|
||||||
}
|
}
|
||||||
@@ -360,13 +517,13 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (socketConnection.PausedActivity)
|
if (socketConnection.PausedActivity)
|
||||||
{
|
{
|
||||||
_logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId);
|
_logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId);
|
||||||
return new CallResult<THandlerResponse>(new ServerError("Socket is paused"));
|
return new CallResult<THandlerResponse>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ct.IsCancellationRequested)
|
if (ct.IsCancellationRequested)
|
||||||
return new CallResult<THandlerResponse>(new CancellationRequestedError());
|
return new CallResult<THandlerResponse>(new CancellationRequestedError());
|
||||||
|
|
||||||
return await socketConnection.SendAndWaitQueryAsync(query, null, ct).ConfigureAwait(false);
|
return await socketConnection.SendAndWaitQueryAsync(query, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -374,13 +531,14 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="socket">The connection to check</param>
|
/// <param name="socket">The connection to check</param>
|
||||||
/// <param name="authenticated">Whether the socket should authenticated</param>
|
/// <param name="authenticated">Whether the socket should authenticated</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult> ConnectIfNeededAsync(SocketConnection socket, bool authenticated)
|
protected virtual async Task<CallResult> ConnectIfNeededAsync(ISocketConnection socket, bool authenticated, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (socket.Connected)
|
if (socket.Connected)
|
||||||
return new CallResult(null);
|
return CallResult.SuccessResult;
|
||||||
|
|
||||||
var connectResult = await ConnectSocketAsync(socket).ConfigureAwait(false);
|
var connectResult = await ConnectSocketAsync(socket, ct).ConfigureAwait(false);
|
||||||
if (!connectResult)
|
if (!connectResult)
|
||||||
return connectResult;
|
return connectResult;
|
||||||
|
|
||||||
@@ -388,9 +546,12 @@ namespace CryptoExchange.Net.Clients
|
|||||||
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
|
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!authenticated || socket.Authenticated)
|
if (!authenticated || socket.Authenticated)
|
||||||
return new CallResult(null);
|
return CallResult.SuccessResult;
|
||||||
|
|
||||||
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
if (socket is not SocketConnection sc)
|
||||||
|
throw new InvalidOperationException("HighPerfSocketConnection not supported for authentication");
|
||||||
|
|
||||||
|
var result = await AuthenticateSocketAsync(sc).ConfigureAwait(false);
|
||||||
if (!result)
|
if (!result)
|
||||||
await socket.CloseAsync().ConfigureAwait(false);
|
await socket.CloseAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -427,14 +588,15 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
|
|
||||||
socket.Authenticated = true;
|
socket.Authenticated = true;
|
||||||
return new CallResult(null);
|
return CallResult.SuccessResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Should return the request which can be used to authenticate a socket connection
|
/// Should return the request which can be used to authenticate a socket connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) => throw new NotImplementedException();
|
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) =>
|
||||||
|
Task.FromResult(AuthenticationProvider!.GetAuthenticationQuery(this, connection));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a system subscription. Used for example to reply to ping requests
|
/// Adds a system subscription. Used for example to reply to ping requests
|
||||||
@@ -443,7 +605,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
protected void AddSystemSubscription(SystemSubscription systemSubscription)
|
protected void AddSystemSubscription(SystemSubscription systemSubscription)
|
||||||
{
|
{
|
||||||
systemSubscriptions.Add(systemSubscription);
|
systemSubscriptions.Add(systemSubscription);
|
||||||
foreach (var connection in socketConnections.Values)
|
foreach (var connection in _socketConnections.Values)
|
||||||
connection.AddSubscription(systemSubscription);
|
connection.AddSubscription(systemSubscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,7 +625,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="connection"></param>
|
/// <param name="connection"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected internal virtual Task<Uri?> GetReconnectUriAsync(SocketConnection connection)
|
protected internal virtual Task<Uri?> GetReconnectUriAsync(ISocketConnection connection)
|
||||||
{
|
{
|
||||||
return Task.FromResult<Uri?>(connection.ConnectionUri);
|
return Task.FromResult<Uri?>(connection.ConnectionUri);
|
||||||
}
|
}
|
||||||
@@ -475,7 +637,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected internal virtual Task<CallResult> RevitalizeRequestAsync(Subscription subscription)
|
protected internal virtual Task<CallResult> RevitalizeRequestAsync(Subscription subscription)
|
||||||
{
|
{
|
||||||
return Task.FromResult(new CallResult(null));
|
return Task.FromResult(CallResult.SuccessResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -484,37 +646,91 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="address">The address the socket is for</param>
|
/// <param name="address">The address the socket is for</param>
|
||||||
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
||||||
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
|
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param>
|
/// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param>
|
||||||
|
/// <param name="individualSubscriptionCount">The number of individual subscriptions in this subscribe request</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection, string? topic = null)
|
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(
|
||||||
|
string address,
|
||||||
|
bool authenticated,
|
||||||
|
bool dedicatedRequestConnection,
|
||||||
|
CancellationToken ct,
|
||||||
|
string? topic = null,
|
||||||
|
int individualSubscriptionCount = 1)
|
||||||
{
|
{
|
||||||
var socketQuery = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
|
var socketQuery = _socketConnections.Where(s => s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
||||||
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
|
||||||
&& s.Value.ApiClient.GetType() == GetType()
|
&& s.Value.ApiClient.GetType() == GetType()
|
||||||
&& (s.Value.Authenticated == authenticated || !authenticated)
|
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic)))
|
||||||
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic))
|
.Select(x => x.Value)
|
||||||
&& s.Value.Connected);
|
.ToList();
|
||||||
|
|
||||||
SocketConnection connection;
|
// If all current socket connections are reconnecting or resubscribing wait for that to finish as we can probably use the existing connection
|
||||||
|
var delayStart = DateTime.UtcNow;
|
||||||
|
var delayed = false;
|
||||||
|
while (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing))
|
||||||
|
{
|
||||||
|
if (DateTime.UtcNow - delayStart > TimeSpan.FromSeconds(10))
|
||||||
|
{
|
||||||
|
if (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing))
|
||||||
|
{
|
||||||
|
// If after this time we still trying to reconnect/reprocess there is some issue in the connection
|
||||||
|
_logger.TimeoutWaitingForReconnectingSocket();
|
||||||
|
return new CallResult<SocketConnection>(new CantConnectError());
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
delayed = true;
|
||||||
|
try { await Task.Delay(50, ct).ConfigureAwait(false); } catch (Exception) { }
|
||||||
|
|
||||||
|
if (ct.IsCancellationRequested)
|
||||||
|
return new CallResult<SocketConnection>(new CancellationRequestedError());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delayed)
|
||||||
|
_logger.WaitedForReconnectingSocket((long)(DateTime.UtcNow - delayStart).TotalMilliseconds);
|
||||||
|
|
||||||
|
socketQuery = socketQuery.Where(s => (s.Status == SocketStatus.None || s.Status == SocketStatus.Connected)
|
||||||
|
&& (s.Authenticated == authenticated || !authenticated)
|
||||||
|
&& s.Connected).ToList();
|
||||||
|
|
||||||
|
SocketConnection? connection;
|
||||||
if (!dedicatedRequestConnection)
|
if (!dedicatedRequestConnection)
|
||||||
{
|
{
|
||||||
connection = socketQuery.Where(s => !s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value;
|
connection = socketQuery.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.UserSubscriptionCount).FirstOrDefault();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault().Value;
|
connection = socketQuery.Where(s => s.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault();
|
||||||
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
|
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
|
||||||
// Mark dedicated request connection as authenticated if the request is authenticated
|
// Mark dedicated request connection as authenticated if the request is authenticated
|
||||||
connection.DedicatedRequestConnection.Authenticated = authenticated;
|
connection.DedicatedRequestConnection.Authenticated = authenticated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool maxConnectionsReached = _socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections);
|
||||||
if (connection != null)
|
if (connection != null)
|
||||||
{
|
{
|
||||||
if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
|
bool lessThanBatchSubCombineTarget = connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget;
|
||||||
|
bool lessThanIndividualSubCombineTarget = connection.Subscriptions.Sum(x => x.IndividualSubscriptionCount) < ClientOptions.SocketIndividualSubscriptionCombineTarget;
|
||||||
|
|
||||||
|
if ((lessThanBatchSubCombineTarget && lessThanIndividualSubCombineTarget)
|
||||||
|
|| maxConnectionsReached)
|
||||||
|
{
|
||||||
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
|
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
|
||||||
return new CallResult<SocketConnection>(connection);
|
// If there is a max subscriptions per connection limit also only use existing if the new subscription doesn't go over the limit
|
||||||
|
if (MaxIndividualSubscriptionsPerConnection == null)
|
||||||
|
return new CallResult<SocketConnection>(connection);
|
||||||
|
|
||||||
|
var currentCount = connection.Subscriptions.Sum(x => x.IndividualSubscriptionCount);
|
||||||
|
if (currentCount + individualSubscriptionCount <= MaxIndividualSubscriptionsPerConnection)
|
||||||
|
return new CallResult<SocketConnection>(connection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (maxConnectionsReached)
|
||||||
|
return new CallResult<SocketConnection>(new InvalidOperationError("Max amount of socket connections reached"));
|
||||||
|
|
||||||
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
||||||
if (!connectionAddress)
|
if (!connectionAddress)
|
||||||
{
|
{
|
||||||
@@ -525,9 +741,8 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (connectionAddress.Data != address)
|
if (connectionAddress.Data != address)
|
||||||
_logger.ConnectionAddressSetTo(connectionAddress.Data!);
|
_logger.ConnectionAddressSetTo(connectionAddress.Data!);
|
||||||
|
|
||||||
// Create new socket
|
// Create new socket connection
|
||||||
var socket = CreateSocket(connectionAddress.Data!);
|
var socketConnection = new SocketConnection(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this, address);
|
||||||
var socketConnection = new SocketConnection(_logger, this, socket, address);
|
|
||||||
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
||||||
socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync;
|
socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync;
|
||||||
if (dedicatedRequestConnection)
|
if (dedicatedRequestConnection)
|
||||||
@@ -548,6 +763,38 @@ namespace CryptoExchange.Net.Clients
|
|||||||
return new CallResult<SocketConnection>(socketConnection);
|
return new CallResult<SocketConnection>(socketConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">The address the socket is for</param>
|
||||||
|
/// <param name="connectionFactory">The factory for creating a socket connection</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual async Task<CallResult<HighPerfSocketConnection<TUpdateType>>> GetHighPerfSocketConnection<TUpdateType>(
|
||||||
|
string address,
|
||||||
|
IHighPerfConnectionFactory connectionFactory,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
var connectionAddress = await GetConnectionUrlAsync(address, false).ConfigureAwait(false);
|
||||||
|
if (!connectionAddress)
|
||||||
|
{
|
||||||
|
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error?.ToString());
|
||||||
|
return connectionAddress.As<HighPerfSocketConnection<TUpdateType>>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectionAddress.Data != address)
|
||||||
|
_logger.ConnectionAddressSetTo(connectionAddress.Data!);
|
||||||
|
|
||||||
|
// Create new socket connection
|
||||||
|
var socketConnection = connectionFactory.CreateHighPerfConnection<TUpdateType>(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this, address);
|
||||||
|
foreach (var ptg in PeriodicTaskRegistrations)
|
||||||
|
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, (con) => ptg.QueryDelegate(con).Request);
|
||||||
|
|
||||||
|
return new CallResult<HighPerfSocketConnection<TUpdateType>>(socketConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Process an unhandled message
|
/// Process an unhandled message
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -561,11 +808,12 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected async virtual Task HandleConnectRateLimitedAsync()
|
protected async virtual Task HandleConnectRateLimitedAsync()
|
||||||
{
|
{
|
||||||
if (ClientOptions.RateLimiterEnabled && RateLimiter is not null && ClientOptions.ConnectDelayAfterRateLimited is not null)
|
if (ClientOptions.RateLimiterEnabled && ClientOptions.ConnectDelayAfterRateLimited.HasValue)
|
||||||
{
|
{
|
||||||
var retryAfter = DateTime.UtcNow.Add(ClientOptions.ConnectDelayAfterRateLimited.Value);
|
var retryAfter = DateTime.UtcNow.Add(ClientOptions.ConnectDelayAfterRateLimited.Value);
|
||||||
_logger.AddingRetryAfterGuard(retryAfter);
|
_logger.AddingRetryAfterGuard(retryAfter);
|
||||||
await RateLimiter.SetRetryAfterGuardAsync(retryAfter, RateLimiting.RateLimitItemType.Connection).ConfigureAwait(false);
|
RateLimiter ??= new RateLimitGate("Connection");
|
||||||
|
await RateLimiter.SetRetryAfterGuardAsync(retryAfter, RateLimitItemType.Connection).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -573,13 +821,17 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// Connect a socket
|
/// Connect a socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="socketConnection">The socket to connect</param>
|
/// <param name="socketConnection">The socket to connect</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult> ConnectSocketAsync(SocketConnection socketConnection)
|
protected virtual async Task<CallResult> ConnectSocketAsync(ISocketConnection socketConnection, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var connectResult = await socketConnection.ConnectAsync().ConfigureAwait(false);
|
var connectResult = await socketConnection.ConnectAsync(ct).ConfigureAwait(false);
|
||||||
if (connectResult)
|
if (connectResult)
|
||||||
{
|
{
|
||||||
socketConnections.TryAdd(socketConnection.SocketId, socketConnection);
|
if (socketConnection is SocketConnection sc)
|
||||||
|
_socketConnections.TryAdd(socketConnection.SocketId, sc);
|
||||||
|
else if (socketConnection is HighPerfSocketConnection hsc)
|
||||||
|
_highPerfSocketConnections.TryAdd(socketConnection.SocketId, hsc);
|
||||||
return connectResult;
|
return connectResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,26 +848,16 @@ namespace CryptoExchange.Net.Clients
|
|||||||
=> new(new Uri(address), ClientOptions.ReconnectPolicy)
|
=> new(new Uri(address), ClientOptions.ReconnectPolicy)
|
||||||
{
|
{
|
||||||
KeepAliveInterval = KeepAliveInterval,
|
KeepAliveInterval = KeepAliveInterval,
|
||||||
|
KeepAliveTimeout = KeepAliveTimeout,
|
||||||
ReconnectInterval = ClientOptions.ReconnectInterval,
|
ReconnectInterval = ClientOptions.ReconnectInterval,
|
||||||
RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null,
|
RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null,
|
||||||
RateLimitingBehavior = ClientOptions.RateLimitingBehaviour,
|
RateLimitingBehavior = ClientOptions.RateLimitingBehaviour,
|
||||||
Proxy = ClientOptions.Proxy,
|
Proxy = ClientOptions.Proxy,
|
||||||
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout,
|
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout,
|
||||||
ReceiveBufferSize = ClientOptions.ReceiveBufferSize,
|
ReceiveBufferSize = ClientOptions.ReceiveBufferSize,
|
||||||
|
UseUpdatedDeserialization = ClientOptions.UseUpdatedDeserialization
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a socket for an address
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="address">The address the socket should connect to</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual IWebsocket CreateSocket(string address)
|
|
||||||
{
|
|
||||||
var socket = SocketFactory.CreateWebsocket(_logger, GetWebSocketParameters(address));
|
|
||||||
_logger.SocketCreatedForAddress(socket.Id, address);
|
|
||||||
return socket;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unsubscribe an update subscription
|
/// Unsubscribe an update subscription
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -625,7 +867,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
Subscription? subscription = null;
|
Subscription? subscription = null;
|
||||||
SocketConnection? connection = null;
|
SocketConnection? connection = null;
|
||||||
foreach (var socket in socketConnections.Values.ToList())
|
foreach (var socket in _socketConnections.Values.ToList())
|
||||||
{
|
{
|
||||||
subscription = socket.GetSubscription(subscriptionId);
|
subscription = socket.GetSubscription(subscriptionId);
|
||||||
if (subscription != null)
|
if (subscription != null)
|
||||||
@@ -663,21 +905,25 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async Task UnsubscribeAllAsync()
|
public virtual async Task UnsubscribeAllAsync()
|
||||||
{
|
{
|
||||||
var sum = socketConnections.Sum(s => s.Value.UserSubscriptionCount);
|
var sum = _socketConnections.Sum(s => s.Value.UserSubscriptionCount) + _highPerfSocketConnections.Sum(s => s.Value.UserSubscriptionCount);
|
||||||
if (sum == 0)
|
if (sum == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_logger.UnsubscribingAll(socketConnections.Sum(s => s.Value.UserSubscriptionCount));
|
_logger.UnsubscribingAll(sum);
|
||||||
var tasks = new List<Task>();
|
var tasks = new List<Task>();
|
||||||
|
|
||||||
|
var socketList = _socketConnections.Values;
|
||||||
|
foreach (var connection in socketList)
|
||||||
{
|
{
|
||||||
var socketList = socketConnections.Values;
|
foreach(var subscription in connection.Subscriptions.Where(x => x.UserSubscription))
|
||||||
foreach (var connection in socketList)
|
tasks.Add(connection.CloseAsync(subscription));
|
||||||
{
|
|
||||||
foreach(var subscription in connection.Subscriptions.Where(x => x.UserSubscription))
|
|
||||||
tasks.Add(connection.CloseAsync(subscription));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var highPerfSocketList = _highPerfSocketConnections.Values;
|
||||||
|
foreach (var connection in highPerfSocketList)
|
||||||
|
tasks.Add(connection.CloseAsync());
|
||||||
|
|
||||||
|
|
||||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -687,10 +933,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async Task ReconnectAsync()
|
public virtual async Task ReconnectAsync()
|
||||||
{
|
{
|
||||||
_logger.ReconnectingAllConnections(socketConnections.Count);
|
_logger.ReconnectingAllConnections(_socketConnections.Count);
|
||||||
var tasks = new List<Task>();
|
var tasks = new List<Task>();
|
||||||
{
|
{
|
||||||
var socketList = socketConnections.Values;
|
var socketList = _socketConnections.Values;
|
||||||
foreach (var sub in socketList)
|
foreach (var sub in socketList)
|
||||||
tasks.Add(sub.TriggerReconnectAsync());
|
tasks.Add(sub.TriggerReconnectAsync());
|
||||||
}
|
}
|
||||||
@@ -703,16 +949,16 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
foreach (var item in DedicatedConnectionConfigs)
|
foreach (var item in DedicatedConnectionConfigs)
|
||||||
{
|
{
|
||||||
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true).ConfigureAwait(false);
|
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true, CancellationToken.None).ConfigureAwait(false);
|
||||||
if (!socketResult)
|
if (!socketResult)
|
||||||
return socketResult.AsDataless();
|
return socketResult.AsDataless();
|
||||||
|
|
||||||
var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated).ConfigureAwait(false);
|
var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated, default).ConfigureAwait(false);
|
||||||
if (!connectResult)
|
if (!connectResult)
|
||||||
return new CallResult(connectResult.Error!);
|
return new CallResult(connectResult.Error!);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new CallResult(null);
|
return CallResult.SuccessResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -722,7 +968,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
base.SetOptions(options);
|
base.SetOptions(options);
|
||||||
|
|
||||||
if ((!previousProxyIsSet && options.Proxy == null)
|
if ((!previousProxyIsSet && options.Proxy == null)
|
||||||
|| socketConnections.IsEmpty)
|
|| _socketConnections.IsEmpty)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -730,7 +976,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
_logger.LogInformation("Reconnecting websockets to apply proxy");
|
_logger.LogInformation("Reconnecting websockets to apply proxy");
|
||||||
|
|
||||||
// Update proxy, also triggers reconnect
|
// Update proxy, also triggers reconnect
|
||||||
foreach (var connection in socketConnections)
|
foreach (var connection in _socketConnections)
|
||||||
_ = connection.Value.UpdateProxy(options.Proxy);
|
_ = connection.Value.UpdateProxy(options.Proxy);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -749,15 +995,15 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public SocketApiClientState GetState(bool includeSubDetails = true)
|
public SocketApiClientState GetState(bool includeSubDetails = true)
|
||||||
{
|
{
|
||||||
var connectionStates = new List<SocketConnection.SocketConnectionState>();
|
var connectionStates = new List<SocketConnectionState>();
|
||||||
foreach (var socketIdAndConnection in socketConnections)
|
foreach (var socketIdAndConnection in _socketConnections)
|
||||||
{
|
{
|
||||||
SocketConnection connection = socketIdAndConnection.Value;
|
SocketConnection connection = socketIdAndConnection.Value;
|
||||||
SocketConnection.SocketConnectionState connectionState = connection.GetState(includeSubDetails);
|
SocketConnectionState connectionState = connection.GetState(includeSubDetails);
|
||||||
connectionStates.Add(connectionState);
|
connectionStates.Add(connectionState);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SocketApiClientState(socketConnections.Count, CurrentSubscriptions, IncomingKbps, connectionStates);
|
return new SocketApiClientState(_socketConnections.Count, CurrentSubscriptions, IncomingKbps, connectionStates);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -771,7 +1017,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
int Connections,
|
int Connections,
|
||||||
int Subscriptions,
|
int Subscriptions,
|
||||||
double DownloadSpeed,
|
double DownloadSpeed,
|
||||||
List<SocketConnection.SocketConnectionState> ConnectionStates)
|
List<SocketConnectionState> ConnectionStates)
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Print the state of the client
|
/// Print the state of the client
|
||||||
@@ -800,9 +1046,9 @@ namespace CryptoExchange.Net.Clients
|
|||||||
cs.SubscriptionStates.ForEach(subState =>
|
cs.SubscriptionStates.ForEach(subState =>
|
||||||
{
|
{
|
||||||
sb.AppendLine($"\t\t\tId: {subState.Id}");
|
sb.AppendLine($"\t\t\tId: {subState.Id}");
|
||||||
sb.AppendLine($"\t\t\tConfirmed: {subState.Confirmed}");
|
sb.AppendLine($"\t\t\tStatus: {subState.Status}");
|
||||||
sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}");
|
sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}");
|
||||||
sb.AppendLine($"\t\t\tIdentifiers: [{string.Join(",", subState.Identifiers)}]");
|
sb.AppendLine($"\t\t\tIdentifiers: [{subState.ListenMatcher.ToString()}]");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -819,7 +1065,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
_disposing = true;
|
_disposing = true;
|
||||||
var tasks = new List<Task>();
|
var tasks = new List<Task>();
|
||||||
{
|
{
|
||||||
var socketList = socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected);
|
var socketList = _socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected);
|
||||||
if (socketList.Any())
|
if (socketList.Any())
|
||||||
_logger.DisposingSocketClient();
|
_logger.DisposingSocketClient();
|
||||||
|
|
||||||
@@ -843,10 +1089,16 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Preprocess a stream message
|
/// Preprocess a stream message
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="connection"></param>
|
public virtual ReadOnlySpan<byte> PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlySpan<byte> data) => data;
|
||||||
/// <param name="type"></param>
|
/// <summary>
|
||||||
/// <param name="data"></param>
|
/// Preprocess a stream message
|
||||||
/// <returns></returns>
|
/// </summary>
|
||||||
public virtual ReadOnlyMemory<byte> PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlyMemory<byte> data) => data;
|
public virtual ReadOnlyMemory<byte> PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlyMemory<byte> data) => data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new message converter instance
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public abstract ISocketMessageHandler CreateMessageConverter(WebSocketMessageType messageType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Balance data
|
|
||||||
/// </summary>
|
|
||||||
public class Balance: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The asset name
|
|
||||||
/// </summary>
|
|
||||||
public string Asset { get; set; } = string.Empty;
|
|
||||||
/// <summary>
|
|
||||||
/// Quantity available
|
|
||||||
/// </summary>
|
|
||||||
public decimal? Available { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Total quantity
|
|
||||||
/// </summary>
|
|
||||||
public decimal? Total { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Base class for common objects
|
|
||||||
/// </summary>
|
|
||||||
public class BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The source object the data is derived from
|
|
||||||
/// </summary>
|
|
||||||
public object SourceObject { get; set; } = null!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Order type
|
|
||||||
/// </summary>
|
|
||||||
public enum CommonOrderType
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Limit type
|
|
||||||
/// </summary>
|
|
||||||
Limit,
|
|
||||||
/// <summary>
|
|
||||||
/// Market type
|
|
||||||
/// </summary>
|
|
||||||
Market,
|
|
||||||
/// <summary>
|
|
||||||
/// Other order type
|
|
||||||
/// </summary>
|
|
||||||
Other
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Order side
|
|
||||||
/// </summary>
|
|
||||||
public enum CommonOrderSide
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Buy order
|
|
||||||
/// </summary>
|
|
||||||
Buy,
|
|
||||||
/// <summary>
|
|
||||||
/// Sell order
|
|
||||||
/// </summary>
|
|
||||||
Sell
|
|
||||||
}
|
|
||||||
/// <summary>
|
|
||||||
/// Order status
|
|
||||||
/// </summary>
|
|
||||||
public enum CommonOrderStatus
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// placed and not fully filled order
|
|
||||||
/// </summary>
|
|
||||||
Active,
|
|
||||||
/// <summary>
|
|
||||||
/// canceled order
|
|
||||||
/// </summary>
|
|
||||||
Canceled,
|
|
||||||
/// <summary>
|
|
||||||
/// filled order
|
|
||||||
/// </summary>
|
|
||||||
Filled
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Position side
|
|
||||||
/// </summary>
|
|
||||||
public enum CommonPositionSide
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Long position
|
|
||||||
/// </summary>
|
|
||||||
Long,
|
|
||||||
/// <summary>
|
|
||||||
/// Short position
|
|
||||||
/// </summary>
|
|
||||||
Short,
|
|
||||||
/// <summary>
|
|
||||||
/// Both
|
|
||||||
/// </summary>
|
|
||||||
Both
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Kline data
|
|
||||||
/// </summary>
|
|
||||||
public class Kline: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Opening time of the kline
|
|
||||||
/// </summary>
|
|
||||||
public DateTime OpenTime { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Price at the open time
|
|
||||||
/// </summary>
|
|
||||||
public decimal? OpenPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Highest price of the kline
|
|
||||||
/// </summary>
|
|
||||||
public decimal? HighPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Lowest price of the kline
|
|
||||||
/// </summary>
|
|
||||||
public decimal? LowPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Close price of the kline
|
|
||||||
/// </summary>
|
|
||||||
public decimal? ClosePrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Volume of the kline
|
|
||||||
/// </summary>
|
|
||||||
public decimal? Volume { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Order data
|
|
||||||
/// </summary>
|
|
||||||
public class Order: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Id of the order
|
|
||||||
/// </summary>
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
/// <summary>
|
|
||||||
/// Symbol of the order
|
|
||||||
/// </summary>
|
|
||||||
public string Symbol { get; set; } = string.Empty;
|
|
||||||
/// <summary>
|
|
||||||
/// Price of the order
|
|
||||||
/// </summary>
|
|
||||||
public decimal? Price { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Quantity of the order
|
|
||||||
/// </summary>
|
|
||||||
public decimal? Quantity { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The quantity of the order which has been filled
|
|
||||||
/// </summary>
|
|
||||||
public decimal? QuantityFilled { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Status of the order
|
|
||||||
/// </summary>
|
|
||||||
public CommonOrderStatus Status { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Side of the order
|
|
||||||
/// </summary>
|
|
||||||
public CommonOrderSide Side { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Type of the order
|
|
||||||
/// </summary>
|
|
||||||
public CommonOrderType Type { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Order time
|
|
||||||
/// </summary>
|
|
||||||
public DateTime Timestamp { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Order book data
|
|
||||||
/// </summary>
|
|
||||||
public class OrderBook: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// List of bids
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<OrderBookEntry> Bids { get; set; } = Array.Empty<OrderBookEntry>();
|
|
||||||
/// <summary>
|
|
||||||
/// List of asks
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<OrderBookEntry> Asks { get; set; } = Array.Empty<OrderBookEntry>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Order book entry
|
|
||||||
/// </summary>
|
|
||||||
public class OrderBookEntry
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Quantity of the entry
|
|
||||||
/// </summary>
|
|
||||||
public decimal Quantity { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Price of the entry
|
|
||||||
/// </summary>
|
|
||||||
public decimal Price { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Id of an order
|
|
||||||
/// </summary>
|
|
||||||
public class OrderId: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Id of an order
|
|
||||||
/// </summary>
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Position data
|
|
||||||
/// </summary>
|
|
||||||
public class Position: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Id of the position
|
|
||||||
/// </summary>
|
|
||||||
public string? Id { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Symbol of the position
|
|
||||||
/// </summary>
|
|
||||||
public string Symbol { get; set; } = string.Empty;
|
|
||||||
/// <summary>
|
|
||||||
/// Leverage
|
|
||||||
/// </summary>
|
|
||||||
public decimal Leverage { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Position quantity
|
|
||||||
/// </summary>
|
|
||||||
public decimal Quantity { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Entry price
|
|
||||||
/// </summary>
|
|
||||||
public decimal? EntryPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Liquidation price
|
|
||||||
/// </summary>
|
|
||||||
public decimal? LiquidationPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Unrealized profit and loss
|
|
||||||
/// </summary>
|
|
||||||
public decimal? UnrealizedPnl { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Realized profit and loss
|
|
||||||
/// </summary>
|
|
||||||
public decimal? RealizedPnl { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Mark price
|
|
||||||
/// </summary>
|
|
||||||
public decimal? MarkPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Auto adding margin
|
|
||||||
/// </summary>
|
|
||||||
public bool? AutoMargin { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Position margin
|
|
||||||
/// </summary>
|
|
||||||
public decimal? PositionMargin { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Position side
|
|
||||||
/// </summary>
|
|
||||||
public CommonPositionSide? Side { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Is isolated
|
|
||||||
/// </summary>
|
|
||||||
public bool? Isolated { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Maintenance margin
|
|
||||||
/// </summary>
|
|
||||||
public decimal? MaintananceMargin { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Symbol data
|
|
||||||
/// </summary>
|
|
||||||
public class Symbol: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Name of the symbol
|
|
||||||
/// </summary>
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
/// <summary>
|
|
||||||
/// Minimal quantity of an order
|
|
||||||
/// </summary>
|
|
||||||
public decimal? MinTradeQuantity { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Step with which the quantity should increase
|
|
||||||
/// </summary>
|
|
||||||
public decimal? QuantityStep { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// step with which the price should increase
|
|
||||||
/// </summary>
|
|
||||||
public decimal? PriceStep { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The max amount of decimals for quantity
|
|
||||||
/// </summary>
|
|
||||||
public int? QuantityDecimals { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The max amount of decimal for price
|
|
||||||
/// </summary>
|
|
||||||
public int? PriceDecimals { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Ticker data
|
|
||||||
/// </summary>
|
|
||||||
public class Ticker: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Symbol
|
|
||||||
/// </summary>
|
|
||||||
public string Symbol { get; set; } = string.Empty;
|
|
||||||
/// <summary>
|
|
||||||
/// Price 24 hours ago
|
|
||||||
/// </summary>
|
|
||||||
public decimal? Price24H { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Last trade price
|
|
||||||
/// </summary>
|
|
||||||
public decimal? LastPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// 24 hour low price
|
|
||||||
/// </summary>
|
|
||||||
public decimal? LowPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// 24 hour high price
|
|
||||||
/// </summary>
|
|
||||||
public decimal? HighPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// 24 hour volume
|
|
||||||
/// </summary>
|
|
||||||
public decimal? Volume { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Trade data
|
|
||||||
/// </summary>
|
|
||||||
public class Trade: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Symbol of the trade
|
|
||||||
/// </summary>
|
|
||||||
public string Symbol { get; set; } = string.Empty;
|
|
||||||
/// <summary>
|
|
||||||
/// Price of the trade
|
|
||||||
/// </summary>
|
|
||||||
public decimal Price { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Quantity of the trade
|
|
||||||
/// </summary>
|
|
||||||
public decimal Quantity { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Timestamp of the trade
|
|
||||||
/// </summary>
|
|
||||||
public DateTime Timestamp { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// User trade info
|
|
||||||
/// </summary>
|
|
||||||
public class UserTrade: Trade
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Id of the trade
|
|
||||||
/// </summary>
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
/// <summary>
|
|
||||||
/// Order id of the trade
|
|
||||||
/// </summary>
|
|
||||||
public string? OrderId { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Fee of the trade
|
|
||||||
/// </summary>
|
|
||||||
public decimal? Fee { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The asset the fee is paid in
|
|
||||||
/// </summary>
|
|
||||||
public string? FeeAsset { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using CryptoExchange.Net.Attributes;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties
|
|
||||||
/// with [ArrayProperty(x)] where x is the index of the property in the array
|
|
||||||
/// </summary>
|
|
||||||
public class ArrayConverter : JsonConverter
|
|
||||||
{
|
|
||||||
private static readonly ConcurrentDictionary<(MemberInfo, Type), Attribute> _attributeByMemberInfoAndTypeCache = new ConcurrentDictionary<(MemberInfo, Type), Attribute>();
|
|
||||||
private static readonly ConcurrentDictionary<(Type, Type), Attribute> _attributeByTypeAndTypeCache = new ConcurrentDictionary<(Type, Type), Attribute>();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool CanConvert(Type objectType)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonToken.Null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (objectType == typeof(JToken))
|
|
||||||
return JToken.Load(reader);
|
|
||||||
|
|
||||||
var result = Activator.CreateInstance(objectType);
|
|
||||||
var arr = JArray.Load(reader);
|
|
||||||
return ParseObject(arr, result!, objectType);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static object ParseObject(JArray arr, object result, Type objectType)
|
|
||||||
{
|
|
||||||
foreach (var property in objectType.GetProperties())
|
|
||||||
{
|
|
||||||
var attribute = GetCustomAttribute<ArrayPropertyAttribute>(property);
|
|
||||||
|
|
||||||
if (attribute == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (attribute.Index >= arr.Count)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (property.PropertyType.BaseType == typeof(Array))
|
|
||||||
{
|
|
||||||
var objType = property.PropertyType.GetElementType();
|
|
||||||
var innerArray = (JArray)arr[attribute.Index];
|
|
||||||
var count = 0;
|
|
||||||
if (innerArray.Count == 0)
|
|
||||||
{
|
|
||||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 0 })!;
|
|
||||||
property.SetValue(result, arrayResult);
|
|
||||||
}
|
|
||||||
else if (innerArray[0].Type == JTokenType.Array)
|
|
||||||
{
|
|
||||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { innerArray.Count })!;
|
|
||||||
foreach (var obj in innerArray)
|
|
||||||
{
|
|
||||||
var innerObj = Activator.CreateInstance(objType!);
|
|
||||||
arrayResult[count] = ParseObject((JArray)obj, innerObj!, objType!);
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
property.SetValue(result, arrayResult);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 })!;
|
|
||||||
var innerObj = Activator.CreateInstance(objType!);
|
|
||||||
arrayResult[0] = ParseObject(innerArray, innerObj!, objType!);
|
|
||||||
property.SetValue(result, arrayResult);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(property) ?? GetCustomAttribute<JsonConverterAttribute>(property.PropertyType);
|
|
||||||
var conversionAttribute = GetCustomAttribute<JsonConversionAttribute>(property) ?? GetCustomAttribute<JsonConversionAttribute>(property.PropertyType);
|
|
||||||
|
|
||||||
object? value;
|
|
||||||
if (converterAttribute != null)
|
|
||||||
{
|
|
||||||
value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)!}});
|
|
||||||
}
|
|
||||||
else if (conversionAttribute != null)
|
|
||||||
{
|
|
||||||
value = arr[attribute.Index].ToObject(property.PropertyType);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
value = arr[attribute.Index];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value != null && property.PropertyType.IsInstanceOfType(value))
|
|
||||||
{
|
|
||||||
property.SetValue(result, value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (value is JToken token)
|
|
||||||
{
|
|
||||||
if (token.Type == JTokenType.Null)
|
|
||||||
value = null;
|
|
||||||
|
|
||||||
if (token.Type == JTokenType.Float)
|
|
||||||
value = token.Value<decimal>();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value is decimal)
|
|
||||||
{
|
|
||||||
property.SetValue(result, value);
|
|
||||||
}
|
|
||||||
else if ((property.PropertyType == typeof(decimal)
|
|
||||||
|| property.PropertyType == typeof(decimal?))
|
|
||||||
&& (value != null && value.ToString()!.IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0))
|
|
||||||
{
|
|
||||||
var v = value.ToString();
|
|
||||||
if (decimal.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
|
|
||||||
property.SetValue(result, dec);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
property.SetValue(result, value == null ? null : Convert.ChangeType(value, property.PropertyType));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
writer.WriteStartArray();
|
|
||||||
var props = value.GetType().GetProperties();
|
|
||||||
var ordered = props.OrderBy(p => GetCustomAttribute<ArrayPropertyAttribute>(p)?.Index);
|
|
||||||
|
|
||||||
var last = -1;
|
|
||||||
foreach (var prop in ordered)
|
|
||||||
{
|
|
||||||
var arrayProp = GetCustomAttribute<ArrayPropertyAttribute>(prop);
|
|
||||||
if (arrayProp == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (arrayProp.Index == last)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
while (arrayProp.Index != last + 1)
|
|
||||||
{
|
|
||||||
writer.WriteValue((string?)null);
|
|
||||||
last += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
last = arrayProp.Index;
|
|
||||||
var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(prop);
|
|
||||||
if (converterAttribute != null)
|
|
||||||
writer.WriteRawValue(JsonConvert.SerializeObject(prop.GetValue(value), (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType)!));
|
|
||||||
else if (!IsSimple(prop.PropertyType))
|
|
||||||
serializer.Serialize(writer, prop.GetValue(value));
|
|
||||||
else
|
|
||||||
writer.WriteValue(prop.GetValue(value));
|
|
||||||
}
|
|
||||||
writer.WriteEndArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsSimple(Type type)
|
|
||||||
{
|
|
||||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
|
||||||
{
|
|
||||||
// nullable type, check if the nested type is simple.
|
|
||||||
return IsSimple(type.GetGenericArguments()[0]);
|
|
||||||
}
|
|
||||||
return type.IsPrimitive
|
|
||||||
|| type.IsEnum
|
|
||||||
|| type == typeof(string)
|
|
||||||
|| type == typeof(decimal);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static T? GetCustomAttribute<T>(MemberInfo memberInfo) where T : Attribute =>
|
|
||||||
(T?)_attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T))!);
|
|
||||||
|
|
||||||
private static T? GetCustomAttribute<T>(Type type) where T : Attribute =>
|
|
||||||
(T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T))!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Base class for enum converters
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">Type of enum to convert</typeparam>
|
|
||||||
public abstract class BaseConverter<T>: JsonConverter where T: struct
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The enum->string mapping
|
|
||||||
/// </summary>
|
|
||||||
protected abstract List<KeyValuePair<T, string>> Mapping { get; }
|
|
||||||
private readonly bool _quotes;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="useQuotes"></param>
|
|
||||||
protected BaseConverter(bool useQuotes)
|
|
||||||
{
|
|
||||||
_quotes = useQuotes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
var stringValue = value == null? null: GetValue((T) value);
|
|
||||||
if (_quotes)
|
|
||||||
writer.WriteValue(stringValue);
|
|
||||||
else
|
|
||||||
writer.WriteRawValue(stringValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
if (reader.Value == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var stringValue = reader.Value.ToString();
|
|
||||||
if (string.IsNullOrWhiteSpace(stringValue))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (!GetValue(stringValue, out var result))
|
|
||||||
{
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {typeof(T)}, Value: {reader.Value}, Known values: {string.Join(", ", Mapping.Select(m => m.Value))}. If you think {reader.Value} should added please open an issue on the Github repo");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a string value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public T ReadString(string data)
|
|
||||||
{
|
|
||||||
return Mapping.FirstOrDefault(v => v.Value == data).Key;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool CanConvert(Type objectType)
|
|
||||||
{
|
|
||||||
// Check if it is type, or nullable of type
|
|
||||||
return objectType == typeof(T) || Nullable.GetUnderlyingType(objectType) == typeof(T);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool GetValue(string value, out T result)
|
|
||||||
{
|
|
||||||
// Check for exact match first, then if not found fallback to a case insensitive match
|
|
||||||
var mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
|
|
||||||
if(mapping.Equals(default(KeyValuePair<T, string>)))
|
|
||||||
mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
|
||||||
|
|
||||||
if (!mapping.Equals(default(KeyValuePair<T, string>)))
|
|
||||||
{
|
|
||||||
result = mapping.Key;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetValue(T value)
|
|
||||||
{
|
|
||||||
return Mapping.FirstOrDefault(v => v.Key.Equals(value)).Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Decimal converter that handles overflowing decimal values (by setting it to decimal.MaxValue)
|
|
||||||
/// </summary>
|
|
||||||
public class BigDecimalConverter : JsonConverter
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool CanConvert(Type objectType)
|
|
||||||
{
|
|
||||||
if (Nullable.GetUnderlyingType(objectType) != null)
|
|
||||||
return Nullable.GetUnderlyingType(objectType) == typeof(decimal);
|
|
||||||
return objectType == typeof(decimal);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonToken.Null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (reader.TokenType == JsonToken.Float || reader.TokenType == JsonToken.Integer)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return decimal.Parse(reader.Value!.ToString()!, NumberStyles.Float, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
catch (OverflowException)
|
|
||||||
{
|
|
||||||
// Value doesn't fit decimal; set it to max value
|
|
||||||
return decimal.MaxValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader.TokenType == JsonToken.String)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var value = reader.Value!.ToString()!;
|
|
||||||
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
catch (OverflowException)
|
|
||||||
{
|
|
||||||
// Value doesn't fit decimal; set it to max value
|
|
||||||
return decimal.MaxValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
writer.WriteValue(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Boolean converter with support for "0"/"1" (strings)
|
|
||||||
/// </summary>
|
|
||||||
public class BoolConverter : JsonConverter
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Determines whether this instance can convert the specified object type.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="objectType">Type of the object.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
|
|
||||||
/// </returns>
|
|
||||||
public override bool CanConvert(Type objectType)
|
|
||||||
{
|
|
||||||
if (Nullable.GetUnderlyingType(objectType) != null)
|
|
||||||
return Nullable.GetUnderlyingType(objectType) == typeof(bool);
|
|
||||||
return objectType == typeof(bool);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reads the JSON representation of the object.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="reader">The <see cref="T:Newtonsoft.Json.JsonReader"/> to read from.</param>
|
|
||||||
/// <param name="objectType">Type of the object.</param>
|
|
||||||
/// <param name="existingValue">The existing value of object being read.</param>
|
|
||||||
/// <param name="serializer">The calling serializer.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// The object value.
|
|
||||||
/// </returns>
|
|
||||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
var value = reader.Value?.ToString()!.ToLower().Trim();
|
|
||||||
if (value == null || value == "")
|
|
||||||
{
|
|
||||||
if (Nullable.GetUnderlyingType(objectType) != null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (value)
|
|
||||||
{
|
|
||||||
case "true":
|
|
||||||
case "yes":
|
|
||||||
case "y":
|
|
||||||
case "1":
|
|
||||||
case "on":
|
|
||||||
return true;
|
|
||||||
case "false":
|
|
||||||
case "no":
|
|
||||||
case "n":
|
|
||||||
case "0":
|
|
||||||
case "off":
|
|
||||||
case "-1":
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we reach here, we're pretty much going to throw an error so let's let Json.NET throw it's pretty-fied error message.
|
|
||||||
return new JsonSerializer().Deserialize(reader, objectType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Specifies that this converter will not participate in writing results.
|
|
||||||
/// </summary>
|
|
||||||
public override bool CanWrite { get { return false; } }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Writes the JSON representation of the object.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="writer">The <see cref="T:Newtonsoft.Json.JsonWriter"/> to write to.</param><param name="value">The value.</param><param name="serializer">The calling serializer.</param>
|
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
using Newtonsoft.Json;
|
|
||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Datetime converter. Supports converting from string/long/double to DateTime and back. Numbers are assumed to be the time since 1970-01-01.
|
|
||||||
/// </summary>
|
|
||||||
public class DateTimeConverter: JsonConverter
|
|
||||||
{
|
|
||||||
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
|
|
||||||
private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000;
|
|
||||||
private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool CanConvert(Type objectType)
|
|
||||||
{
|
|
||||||
return objectType == typeof(DateTime) || objectType == typeof(DateTime?);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
if (reader.Value == null)
|
|
||||||
{
|
|
||||||
if (objectType == typeof(DateTime))
|
|
||||||
return default(DateTime);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(reader.TokenType is JsonToken.Integer)
|
|
||||||
{
|
|
||||||
var longValue = (long)reader.Value;
|
|
||||||
if (longValue == 0 || longValue == -1)
|
|
||||||
return objectType == typeof(DateTime) ? default(DateTime): null;
|
|
||||||
|
|
||||||
return ParseFromLong(longValue);
|
|
||||||
}
|
|
||||||
else if (reader.TokenType is JsonToken.Float)
|
|
||||||
{
|
|
||||||
var doubleValue = (double)reader.Value;
|
|
||||||
if (doubleValue == 0 || doubleValue == -1)
|
|
||||||
return objectType == typeof(DateTime) ? default(DateTime) : null;
|
|
||||||
|
|
||||||
if (doubleValue < 19999999999)
|
|
||||||
return ConvertFromSeconds(doubleValue);
|
|
||||||
|
|
||||||
return ConvertFromMilliseconds(doubleValue);
|
|
||||||
}
|
|
||||||
else if(reader.TokenType is JsonToken.String)
|
|
||||||
{
|
|
||||||
var stringValue = (string)reader.Value;
|
|
||||||
if (string.IsNullOrWhiteSpace(stringValue)
|
|
||||||
|| stringValue == "-1"
|
|
||||||
|| (double.TryParse(stringValue, out var doubleVal) && doubleVal == 0))
|
|
||||||
{
|
|
||||||
return objectType == typeof(DateTime) ? default(DateTime) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ParseFromString(stringValue);
|
|
||||||
}
|
|
||||||
else if(reader.TokenType == JsonToken.Date)
|
|
||||||
{
|
|
||||||
return (DateTime)reader.Value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parse a long value to datetime
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="longValue"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ParseFromLong(long longValue)
|
|
||||||
{
|
|
||||||
if (longValue < 19999999999)
|
|
||||||
return ConvertFromSeconds(longValue);
|
|
||||||
if (longValue < 19999999999999)
|
|
||||||
return ConvertFromMilliseconds(longValue);
|
|
||||||
if (longValue < 19999999999999999)
|
|
||||||
return ConvertFromMicroseconds(longValue);
|
|
||||||
|
|
||||||
return ConvertFromNanoseconds(longValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parse a string value to datetime
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="stringValue"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ParseFromString(string stringValue)
|
|
||||||
{
|
|
||||||
if (stringValue.Length == 12 && stringValue.StartsWith("202"))
|
|
||||||
{
|
|
||||||
// Parse 202303261200 format
|
|
||||||
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
|
||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
|
||||||
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|
|
||||||
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|
|
||||||
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
|
|
||||||
{
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stringValue.Length == 8)
|
|
||||||
{
|
|
||||||
// Parse 20211103 format
|
|
||||||
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
|
||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
|
||||||
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
|
|
||||||
{
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stringValue.Length == 6)
|
|
||||||
{
|
|
||||||
// Parse 211103 format
|
|
||||||
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|
|
||||||
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|
|
||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
|
|
||||||
{
|
|
||||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
|
||||||
{
|
|
||||||
// Parse 1637745563.000 format
|
|
||||||
if (doubleValue < 19999999999)
|
|
||||||
return ConvertFromSeconds(doubleValue);
|
|
||||||
if (doubleValue < 19999999999999)
|
|
||||||
return ConvertFromMilliseconds((long)doubleValue);
|
|
||||||
if (doubleValue < 19999999999999999)
|
|
||||||
return ConvertFromMicroseconds((long)doubleValue);
|
|
||||||
|
|
||||||
return ConvertFromNanoseconds((long)doubleValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stringValue.Length == 10)
|
|
||||||
{
|
|
||||||
// Parse 2021-11-03 format
|
|
||||||
var values = stringValue.Split('-');
|
|
||||||
if (!int.TryParse(values[0], out var year)
|
|
||||||
|| !int.TryParse(values[1], out var month)
|
|
||||||
|| !int.TryParse(values[2], out var day))
|
|
||||||
{
|
|
||||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="seconds"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ConvertFromSeconds(double seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="milliseconds"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ConvertFromMilliseconds(double milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="microseconds"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ConvertFromMicroseconds(long microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="nanoseconds"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ConvertFromNanoseconds(long nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="time"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("time")]
|
|
||||||
public static long? ConvertToSeconds(DateTime? time) => time == null ? null: (long)Math.Round((time.Value - _epoch).TotalSeconds);
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a DateTime value to milliseconds since epoch (01-01-1970) value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="time"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("time")]
|
|
||||||
public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds);
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a DateTime value to microseconds since epoch (01-01-1970) value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="time"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("time")]
|
|
||||||
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond);
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="time"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("time")]
|
|
||||||
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
|
|
||||||
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
var datetimeValue = (DateTime?)value;
|
|
||||||
if (datetimeValue == null)
|
|
||||||
writer.WriteValue((DateTime?)null);
|
|
||||||
if(datetimeValue == default(DateTime))
|
|
||||||
writer.WriteValue((DateTime?)null);
|
|
||||||
else
|
|
||||||
writer.WriteValue((long)Math.Round(((DateTime)value! - new DateTime(1970, 1, 1)).TotalMilliseconds));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
using Newtonsoft.Json;
|
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Converter for serializing decimal values as string
|
|
||||||
/// </summary>
|
|
||||||
public class DecimalStringWriterConverter : JsonConverter
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool CanRead => false;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool CanConvert(Type objectType) => objectType == typeof(decimal) || objectType == typeof(decimal?);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => writer.WriteValue(((decimal?)value)?.ToString(CultureInfo.InvariantCulture) ?? null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
using CryptoExchange.Net.Attributes;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
|
|
||||||
/// </summary>
|
|
||||||
public class EnumConverter : JsonConverter
|
|
||||||
{
|
|
||||||
private bool _warnOnMissingEntry = true;
|
|
||||||
private bool _writeAsInt;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// </summary>
|
|
||||||
public EnumConverter() { }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="writeAsInt"></param>
|
|
||||||
/// <param name="warnOnMissingEntry"></param>
|
|
||||||
public EnumConverter(bool writeAsInt, bool warnOnMissingEntry)
|
|
||||||
{
|
|
||||||
_writeAsInt = writeAsInt;
|
|
||||||
_warnOnMissingEntry = warnOnMissingEntry;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<Type, List<KeyValuePair<object, string>>> _mapping = new();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool CanConvert(Type objectType)
|
|
||||||
{
|
|
||||||
return objectType.IsEnum || Nullable.GetUnderlyingType(objectType)?.IsEnum == true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
var enumType = Nullable.GetUnderlyingType(objectType) ?? objectType;
|
|
||||||
if (!_mapping.TryGetValue(enumType, out var mapping))
|
|
||||||
mapping = AddMapping(enumType);
|
|
||||||
|
|
||||||
var stringValue = reader.Value?.ToString();
|
|
||||||
if (stringValue == null || stringValue == "")
|
|
||||||
{
|
|
||||||
// Received null value
|
|
||||||
var emptyResult = GetDefaultValue(objectType, enumType);
|
|
||||||
if(emptyResult != null)
|
|
||||||
// If the property we're parsing to isn't nullable there isn't a correct way to return this as null will either throw an exception (.net framework) or the default enum value (dotnet core).
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null enum value, but property type is not a nullable enum. EnumType: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo");
|
|
||||||
|
|
||||||
return emptyResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!GetValue(enumType, mapping, stringValue!, out var result))
|
|
||||||
{
|
|
||||||
var defaultValue = GetDefaultValue(objectType, enumType);
|
|
||||||
if (string.IsNullOrWhiteSpace(stringValue))
|
|
||||||
{
|
|
||||||
if (defaultValue != null)
|
|
||||||
// We received an empty string and have no mapping for it, and the property isn't nullable
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received empty string as enum value, but property type is not a nullable enum. EnumType: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// We received an enum value but weren't able to parse it.
|
|
||||||
if (_warnOnMissingEntry)
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {enumType.Name}, Value: {reader.Value}, Known values: {string.Join(", ", mapping.Select(m => m.Value))}. If you think {reader.Value} should added please open an issue on the Github repo");
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static object? GetDefaultValue(Type objectType, Type enumType)
|
|
||||||
{
|
|
||||||
if (Nullable.GetUnderlyingType(objectType) != null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return Activator.CreateInstance(enumType); // return default value
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<KeyValuePair<object, string>> AddMapping(Type objectType)
|
|
||||||
{
|
|
||||||
var mapping = new List<KeyValuePair<object, string>>();
|
|
||||||
var enumMembers = objectType.GetMembers();
|
|
||||||
foreach (var member in enumMembers)
|
|
||||||
{
|
|
||||||
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
|
|
||||||
foreach (MapAttribute attribute in maps)
|
|
||||||
{
|
|
||||||
foreach (var value in attribute.Values)
|
|
||||||
mapping.Add(new KeyValuePair<object, string>(Enum.Parse(objectType, member.Name), value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_mapping.TryAdd(objectType, mapping);
|
|
||||||
return mapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool GetValue(Type objectType, List<KeyValuePair<object, string>> enumMapping, string value, out object? result)
|
|
||||||
{
|
|
||||||
// Check for exact match first, then if not found fallback to a case insensitive match
|
|
||||||
var mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
|
|
||||||
if (mapping.Equals(default(KeyValuePair<object, string>)))
|
|
||||||
mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
|
||||||
|
|
||||||
if (!mapping.Equals(default(KeyValuePair<object, string>)))
|
|
||||||
{
|
|
||||||
result = mapping.Key;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// If no explicit mapping is found try to parse string
|
|
||||||
result = Enum.Parse(objectType, value, true);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T"></typeparam>
|
|
||||||
/// <param name="enumValue"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("enumValue")]
|
|
||||||
public static string? GetString<T>(T enumValue) => GetString(typeof(T), enumValue);
|
|
||||||
|
|
||||||
|
|
||||||
[return: NotNullIfNotNull("enumValue")]
|
|
||||||
private static string? GetString(Type objectType, object? enumValue)
|
|
||||||
{
|
|
||||||
objectType = Nullable.GetUnderlyingType(objectType) ?? objectType;
|
|
||||||
|
|
||||||
if (!_mapping.TryGetValue(objectType, out var mapping))
|
|
||||||
mapping = AddMapping(objectType);
|
|
||||||
|
|
||||||
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
{
|
|
||||||
writer.WriteNull();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!_writeAsInt)
|
|
||||||
{
|
|
||||||
var stringValue = GetString(value.GetType(), value);
|
|
||||||
writer.WriteValue(stringValue);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
writer.WriteValue((int)value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,338 +0,0 @@
|
|||||||
using CryptoExchange.Net.Converters.MessageParsing;
|
|
||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Json.Net message accessor
|
|
||||||
/// </summary>
|
|
||||||
public abstract class JsonNetMessageAccessor : IMessageAccessor
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The json token loaded
|
|
||||||
/// </summary>
|
|
||||||
protected JToken? _token;
|
|
||||||
private static readonly JsonSerializer _serializer = JsonSerializer.Create(SerializerOptions.WithConverters);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool IsJson { get; protected set; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract bool OriginalDataAvailable { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public object? Underlying => _token;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
|
|
||||||
{
|
|
||||||
if (!IsJson)
|
|
||||||
return new CallResult<object>(GetOriginalString());
|
|
||||||
|
|
||||||
var source = _token;
|
|
||||||
if (path != null)
|
|
||||||
source = GetPathNode(path.Value);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = source!.ToObject(type, _serializer)!;
|
|
||||||
return new CallResult<object>(result);
|
|
||||||
}
|
|
||||||
catch (JsonReaderException jre)
|
|
||||||
{
|
|
||||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
|
|
||||||
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
|
||||||
}
|
|
||||||
catch (JsonSerializationException jse)
|
|
||||||
{
|
|
||||||
var info = $"Deserialize JsonSerializationException: {jse.Message}";
|
|
||||||
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var exceptionInfo = ex.ToLogString();
|
|
||||||
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
|
|
||||||
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public CallResult<T> Deserialize<T>(MessagePath? path = null)
|
|
||||||
{
|
|
||||||
var source = _token;
|
|
||||||
if (path != null)
|
|
||||||
source = GetPathNode(path.Value);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = source!.ToObject<T>(_serializer)!;
|
|
||||||
return new CallResult<T>(result);
|
|
||||||
}
|
|
||||||
catch (JsonReaderException jre)
|
|
||||||
{
|
|
||||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
|
|
||||||
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
|
||||||
}
|
|
||||||
catch (JsonSerializationException jse)
|
|
||||||
{
|
|
||||||
var info = $"Deserialize JsonSerializationException: {jse.Message}";
|
|
||||||
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var exceptionInfo = ex.ToLogString();
|
|
||||||
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
|
|
||||||
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public NodeType? GetNodeType()
|
|
||||||
{
|
|
||||||
if (!IsJson)
|
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
|
||||||
|
|
||||||
if (_token == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (_token.Type == JTokenType.Object)
|
|
||||||
return NodeType.Object;
|
|
||||||
|
|
||||||
if (_token.Type == JTokenType.Array)
|
|
||||||
return NodeType.Array;
|
|
||||||
|
|
||||||
return NodeType.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public NodeType? GetNodeType(MessagePath path)
|
|
||||||
{
|
|
||||||
if (!IsJson)
|
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
|
||||||
|
|
||||||
var node = GetPathNode(path);
|
|
||||||
if (node == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (node.Type == JTokenType.Object)
|
|
||||||
return NodeType.Object;
|
|
||||||
|
|
||||||
if (node.Type == JTokenType.Array)
|
|
||||||
return NodeType.Array;
|
|
||||||
|
|
||||||
return NodeType.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public T? GetValue<T>(MessagePath path)
|
|
||||||
{
|
|
||||||
if (!IsJson)
|
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
|
||||||
|
|
||||||
var value = GetPathNode(path);
|
|
||||||
if (value == null)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
if (value.Type == JTokenType.Object || value.Type == JTokenType.Array)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
return value!.Value<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public List<T?>? GetValues<T>(MessagePath path)
|
|
||||||
{
|
|
||||||
if (!IsJson)
|
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
|
||||||
|
|
||||||
var value = GetPathNode(path);
|
|
||||||
if (value == null)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
if (value.Type == JTokenType.Object)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
return value!.Values<T>().ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private JToken? GetPathNode(MessagePath path)
|
|
||||||
{
|
|
||||||
if (!IsJson)
|
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
|
||||||
|
|
||||||
var currentToken = _token;
|
|
||||||
foreach (var node in path)
|
|
||||||
{
|
|
||||||
if (node.Type == 0)
|
|
||||||
{
|
|
||||||
// Int value
|
|
||||||
var val = node.Index!.Value;
|
|
||||||
if (currentToken!.Type != JTokenType.Array || ((JArray)currentToken).Count <= val)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
currentToken = currentToken[val];
|
|
||||||
}
|
|
||||||
else if (node.Type == 1)
|
|
||||||
{
|
|
||||||
// String value
|
|
||||||
if (currentToken!.Type != JTokenType.Object)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
currentToken = currentToken[node.Property!];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Property name
|
|
||||||
if (currentToken!.Type != JTokenType.Object)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
currentToken = (currentToken.First as JProperty)?.Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentToken == null)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract string GetOriginalString();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract void Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Json.Net stream message accessor
|
|
||||||
/// </summary>
|
|
||||||
public class JsonNetStreamMessageAccessor : JsonNetMessageAccessor, IStreamMessageAccessor
|
|
||||||
{
|
|
||||||
private Stream? _stream;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<CallResult> Read(Stream stream, bool bufferStream)
|
|
||||||
{
|
|
||||||
if (bufferStream && stream is not MemoryStream)
|
|
||||||
{
|
|
||||||
// We need to be buffer the stream, and it's not currently a seekable stream, so copy it to a new memory stream
|
|
||||||
_stream = new MemoryStream();
|
|
||||||
stream.CopyTo(_stream);
|
|
||||||
_stream.Position = 0;
|
|
||||||
}
|
|
||||||
else if (bufferStream)
|
|
||||||
{
|
|
||||||
// We need to buffer the stream, and the current stream is seekable, store as is
|
|
||||||
_stream = stream;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// We don't need to buffer the stream, so don't bother keeping the reference
|
|
||||||
}
|
|
||||||
|
|
||||||
var readStream = _stream ?? stream;
|
|
||||||
var length = readStream.CanSeek ? readStream.Length : 4096;
|
|
||||||
using var reader = new StreamReader(readStream, Encoding.UTF8, false, (int)Math.Max(2, length), true);
|
|
||||||
using var jsonTextReader = new JsonTextReader(reader);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_token = await JToken.LoadAsync(jsonTextReader).ConfigureAwait(false);
|
|
||||||
IsJson = true;
|
|
||||||
return new CallResult(null);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Not a json message
|
|
||||||
IsJson = false;
|
|
||||||
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string GetOriginalString()
|
|
||||||
{
|
|
||||||
if (_stream is null)
|
|
||||||
throw new NullReferenceException("Stream not initialized");
|
|
||||||
|
|
||||||
_stream.Position = 0;
|
|
||||||
using var textReader = new StreamReader(_stream, Encoding.UTF8, false, 1024, true);
|
|
||||||
return textReader.ReadToEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Clear()
|
|
||||||
{
|
|
||||||
_stream?.Dispose();
|
|
||||||
_stream = null;
|
|
||||||
_token = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Json.Net byte message accessor
|
|
||||||
/// </summary>
|
|
||||||
public class JsonNetByteMessageAccessor : JsonNetMessageAccessor, IByteMessageAccessor
|
|
||||||
{
|
|
||||||
private ReadOnlyMemory<byte> _bytes;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public CallResult Read(ReadOnlyMemory<byte> data)
|
|
||||||
{
|
|
||||||
_bytes = data;
|
|
||||||
|
|
||||||
// Try getting the underlying byte[] instead of the ToArray to prevent creating a copy
|
|
||||||
using var stream = MemoryMarshal.TryGetArray(data, out var arraySegment)
|
|
||||||
? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count)
|
|
||||||
: new MemoryStream(data.ToArray());
|
|
||||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, Math.Max(2, data.Length), true);
|
|
||||||
using var jsonTextReader = new JsonTextReader(reader);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_token = JToken.Load(jsonTextReader);
|
|
||||||
IsJson = true;
|
|
||||||
return new CallResult(null);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Not a json message
|
|
||||||
IsJson = false;
|
|
||||||
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string GetOriginalString() =>
|
|
||||||
// Netstandard 2.0 doesn't support GetString from a ReadonlySpan<byte>, so use ToArray there instead
|
|
||||||
#if NETSTANDARD2_0
|
|
||||||
Encoding.UTF8.GetString(_bytes.ToArray());
|
|
||||||
#else
|
|
||||||
Encoding.UTF8.GetString(_bytes.Span);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool OriginalDataAvailable => true;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Clear()
|
|
||||||
{
|
|
||||||
_bytes = null;
|
|
||||||
_token = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public class JsonNetMessageSerializer : IMessageSerializer
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public string Serialize(object message) => JsonConvert.SerializeObject(message, Formatting.None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
using Newtonsoft.Json;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Serializer options
|
|
||||||
/// </summary>
|
|
||||||
public static class SerializerOptions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Json serializer settings which includes the EnumConverter, DateTimeConverter and BoolConverter
|
|
||||||
/// </summary>
|
|
||||||
public static JsonSerializerSettings WithConverters => new JsonSerializerSettings
|
|
||||||
{
|
|
||||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
|
||||||
Culture = CultureInfo.InvariantCulture,
|
|
||||||
Converters =
|
|
||||||
{
|
|
||||||
new EnumConverter(),
|
|
||||||
new DateTimeConverter(),
|
|
||||||
new BoolConverter()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Default json serializer settings
|
|
||||||
/// </summary>
|
|
||||||
public static JsonSerializerSettings Default => new JsonSerializerSettings
|
|
||||||
{
|
|
||||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
|
||||||
Culture = CultureInfo.InvariantCulture
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// REST message handler
|
||||||
|
/// </summary>
|
||||||
|
public interface IRestMessageHandler
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The `accept` HTTP response header for the request
|
||||||
|
/// </summary>
|
||||||
|
MediaTypeWithQualityHeaderValue AcceptHeader { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether a seekable stream is required
|
||||||
|
/// </summary>
|
||||||
|
bool RequiresSeekableStream { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse the response when the HTTP response status indicated an error
|
||||||
|
/// </summary>
|
||||||
|
ValueTask<Error> ParseErrorResponse(
|
||||||
|
int httpStatusCode,
|
||||||
|
HttpResponseHeaders responseHeaders,
|
||||||
|
Stream responseStream);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse the response when the HTTP response status indicated a rate limit error
|
||||||
|
/// </summary>
|
||||||
|
ValueTask<ServerRateLimitError> ParseErrorRateLimitResponse(
|
||||||
|
int httpStatusCode,
|
||||||
|
HttpResponseHeaders responseHeaders,
|
||||||
|
Stream responseStream);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if the response is an error response; if so return the error.<br />
|
||||||
|
/// Note that if the API returns a standard result wrapper, something like this:
|
||||||
|
/// <code>{ "code": 400, "msg": "error", "data": {} }</code>
|
||||||
|
/// then the `CheckDeserializedResponse` method should be used for checking the result
|
||||||
|
/// </summary>
|
||||||
|
ValueTask<Error?> CheckForErrorResponse(
|
||||||
|
RequestDefinition request,
|
||||||
|
HttpResponseHeaders responseHeaders,
|
||||||
|
Stream responseStream);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserialize the response stream
|
||||||
|
/// </summary>
|
||||||
|
ValueTask<(T? Result, Error? Error)> TryDeserializeAsync<T>(
|
||||||
|
Stream responseStream,
|
||||||
|
CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check whether the resulting T object indicates an error or not
|
||||||
|
/// </summary>
|
||||||
|
Error? CheckDeserializedResponse<T>(HttpResponseHeaders responseHeaders, T result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// WebSocket message handler
|
||||||
|
/// </summary>
|
||||||
|
public interface ISocketMessageHandler
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get an identifier for the message which can be used to determine the type of the message
|
||||||
|
/// </summary>
|
||||||
|
string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get optional topic filter, for example a symbol name
|
||||||
|
/// </summary>
|
||||||
|
string? GetTopicFilter(object deserializedObject);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserialize to the provided type
|
||||||
|
/// </summary>
|
||||||
|
object Deserialize(ReadOnlySpan<byte> data, Type type);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Message type definition
|
||||||
|
/// </summary>
|
||||||
|
public class MessageTypeDefinition
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to immediately select the definition when it is matched. Can only be used when the evaluator has a single unique field to look for
|
||||||
|
/// </summary>
|
||||||
|
public bool ForceIfFound { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The fields a message needs to contain for this definition
|
||||||
|
/// </summary>
|
||||||
|
public MessageFieldReference[] Fields { get; set; } = [];
|
||||||
|
/// <summary>
|
||||||
|
/// The callback for getting the identifier string
|
||||||
|
/// </summary>
|
||||||
|
public Func<SearchResult, string>? TypeIdentifierCallback { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The static identifier string to return when this evaluator is matched
|
||||||
|
/// </summary>
|
||||||
|
public string? StaticIdentifier { get; set; }
|
||||||
|
|
||||||
|
internal string? GetMessageType(SearchResult result)
|
||||||
|
{
|
||||||
|
if (StaticIdentifier != null)
|
||||||
|
return StaticIdentifier;
|
||||||
|
|
||||||
|
return TypeIdentifierCallback!(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool Satisfied(SearchResult result)
|
||||||
|
{
|
||||||
|
foreach(var field in Fields)
|
||||||
|
{
|
||||||
|
if (!result.Contains(field))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||||
|
{
|
||||||
|
internal class MessageEvalutorFieldReference
|
||||||
|
{
|
||||||
|
public bool SkipReading { get; set; }
|
||||||
|
public bool OverlappingField { get; set; }
|
||||||
|
public MessageFieldReference Field { get; set; }
|
||||||
|
public MessageTypeDefinition? ForceEvaluator { get; set; }
|
||||||
|
|
||||||
|
public MessageEvalutorFieldReference(MessageFieldReference field)
|
||||||
|
{
|
||||||
|
Field = field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Reference to a message field
|
||||||
|
/// </summary>
|
||||||
|
public abstract class MessageFieldReference
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The name for this search field
|
||||||
|
/// </summary>
|
||||||
|
public string SearchName { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The depth at which to look for this field
|
||||||
|
/// </summary>
|
||||||
|
public int Depth { get; set; } = 1;
|
||||||
|
/// <summary>
|
||||||
|
/// Callback to check if the field value matches an expected constraint
|
||||||
|
/// </summary>
|
||||||
|
public Func<string?, bool>? Constraint { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check whether the value is one of the string values in the set
|
||||||
|
/// </summary>
|
||||||
|
public MessageFieldReference WithFilterConstraint(HashSet<string?> set)
|
||||||
|
{
|
||||||
|
Constraint = set.Contains;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check whether the value is equal to a string
|
||||||
|
/// </summary>
|
||||||
|
public MessageFieldReference WithEqualConstraint(string compare)
|
||||||
|
{
|
||||||
|
Constraint = x => x != null && x.Equals(compare, StringComparison.Ordinal);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check whether the value is not equal to a string
|
||||||
|
/// </summary>
|
||||||
|
public MessageFieldReference WithNotEqualConstraint(string compare)
|
||||||
|
{
|
||||||
|
Constraint = x => x == null || !x.Equals(compare, StringComparison.Ordinal);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check whether the value is not null
|
||||||
|
/// </summary>
|
||||||
|
public MessageFieldReference WithNotNullConstraint()
|
||||||
|
{
|
||||||
|
Constraint = x => x != null;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check whether the value starts with a certain string
|
||||||
|
/// </summary>
|
||||||
|
public MessageFieldReference WithStartsWithConstraint(string start)
|
||||||
|
{
|
||||||
|
Constraint = x => x != null && x.StartsWith(start, StringComparison.Ordinal);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check whether the value starts with a certain string
|
||||||
|
/// </summary>
|
||||||
|
public MessageFieldReference WithStartsWithConstraints(params string[] startValues)
|
||||||
|
{
|
||||||
|
Constraint = x =>
|
||||||
|
{
|
||||||
|
if (x == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
foreach (var item in startValues)
|
||||||
|
{
|
||||||
|
if (x!.StartsWith(item, StringComparison.Ordinal))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check whether the value starts with a certain string
|
||||||
|
/// </summary>
|
||||||
|
public MessageFieldReference WithCustomConstraint(Func<string?, bool> constraint)
|
||||||
|
{
|
||||||
|
Constraint = constraint;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public MessageFieldReference(string searchName)
|
||||||
|
{
|
||||||
|
SearchName = searchName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reference to a property message field
|
||||||
|
/// </summary>
|
||||||
|
public class PropertyFieldReference : MessageFieldReference
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The property name in the JSON
|
||||||
|
/// </summary>
|
||||||
|
public byte[] PropertyName { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the property value is array values
|
||||||
|
/// </summary>
|
||||||
|
public bool ArrayValues { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public PropertyFieldReference(string propertyName) : base(propertyName)
|
||||||
|
{
|
||||||
|
PropertyName = Encoding.UTF8.GetBytes(propertyName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reference to an array message field
|
||||||
|
/// </summary>
|
||||||
|
public class ArrayFieldReference : MessageFieldReference
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The index in the array
|
||||||
|
/// </summary>
|
||||||
|
public int ArrayIndex { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public ArrayFieldReference(string searchName, int depth, int index) : base(searchName)
|
||||||
|
{
|
||||||
|
Depth = depth;
|
||||||
|
ArrayIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The results of a search for fields in a JSON message
|
||||||
|
/// </summary>
|
||||||
|
public class SearchResult
|
||||||
|
{
|
||||||
|
private List<SearchResultItem> _items = new List<SearchResultItem>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the value of a field
|
||||||
|
/// </summary>
|
||||||
|
public string? FieldValue(string searchName)
|
||||||
|
{
|
||||||
|
foreach (var item in _items)
|
||||||
|
{
|
||||||
|
if (item.Field.SearchName.Equals(searchName, StringComparison.Ordinal))
|
||||||
|
return item.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception($"No field value found for {searchName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of found search field values
|
||||||
|
/// </summary>
|
||||||
|
public int Count => _items.Count;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clear the search result
|
||||||
|
/// </summary>
|
||||||
|
public void Clear() => _items.Clear();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the value for a specific field was found
|
||||||
|
/// </summary>
|
||||||
|
public bool Contains(MessageFieldReference field)
|
||||||
|
{
|
||||||
|
foreach (var item in _items)
|
||||||
|
{
|
||||||
|
if (item.Field == field)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Write a value to the result
|
||||||
|
/// </summary>
|
||||||
|
public void Write(MessageFieldReference field, string? value) => _items.Add(new SearchResultItem
|
||||||
|
{
|
||||||
|
Field = field,
|
||||||
|
Value = value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Search result value
|
||||||
|
/// </summary>
|
||||||
|
public struct SearchResultItem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The field the values is for
|
||||||
|
/// </summary>
|
||||||
|
public MessageFieldReference Field { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The value of the field
|
||||||
|
/// </summary>
|
||||||
|
public string? Value { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
using System;
|
using CryptoExchange.Net.Exceptions;
|
||||||
using System.Collections.Concurrent;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
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;
|
using System.Text.Json;
|
||||||
using CryptoExchange.Net.Attributes;
|
using System.Text.Json.Serialization;
|
||||||
using System.Collections.Generic;
|
using System.Threading;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
{
|
{
|
||||||
@@ -14,51 +15,38 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties
|
/// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties
|
||||||
/// with [ArrayProperty(x)] where x is the index of the property in the array
|
/// with [ArrayProperty(x)] where x is the index of the property in the array
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ArrayConverter : JsonConverterFactory
|
#if NET5_0_OR_GREATER
|
||||||
|
public class ArrayConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : JsonConverter<T> where T : new()
|
||||||
|
#else
|
||||||
|
public class ArrayConverter<T> : JsonConverter<T> where T : new()
|
||||||
|
#endif
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
private static SortedDictionary<int, List<ArrayPropertyInfo>>? _typePropertyInfo;
|
||||||
public override bool CanConvert(Type typeToConvert) => true;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions 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")]
|
||||||
|
#endif
|
||||||
|
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
Type converterType = typeof(ArrayConverterInner<>).MakeGenericType(typeToConvert);
|
if (value == null)
|
||||||
return (JsonConverter)Activator.CreateInstance(converterType)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ArrayPropertyInfo
|
|
||||||
{
|
|
||||||
public PropertyInfo PropertyInfo { get; set; } = null!;
|
|
||||||
public ArrayPropertyAttribute ArrayProperty { get; set; } = null!;
|
|
||||||
public Type? JsonConverterType { get; set; }
|
|
||||||
public bool DefaultDeserialization { get; set; }
|
|
||||||
public Type TargetType { get; set; } = null!;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ArrayConverterInner<T> : JsonConverter<T>
|
|
||||||
{
|
|
||||||
private static readonly ConcurrentDictionary<Type, List<ArrayPropertyInfo>> _typeAttributesCache = new ConcurrentDictionary<Type, List<ArrayPropertyInfo>>();
|
|
||||||
private static readonly ConcurrentDictionary<Type, JsonSerializerOptions> _converterOptionsCache = new ConcurrentDictionary<Type, JsonSerializerOptions>();
|
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
|
||||||
{
|
{
|
||||||
if (value == null)
|
writer.WriteNullValue();
|
||||||
{
|
return;
|
||||||
writer.WriteNullValue();
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.WriteStartArray();
|
if (_typePropertyInfo == null)
|
||||||
|
_typePropertyInfo = CacheTypeAttributes();
|
||||||
|
|
||||||
var valueType = value.GetType();
|
writer.WriteStartArray();
|
||||||
if (!_typeAttributesCache.TryGetValue(valueType, out var typeAttributes))
|
var last = -1;
|
||||||
typeAttributes = CacheTypeAttributes(valueType);
|
foreach (var indexProps in _typePropertyInfo)
|
||||||
|
{
|
||||||
var ordered = typeAttributes.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index);
|
foreach (var prop in indexProps.Value)
|
||||||
var last = -1;
|
|
||||||
foreach (var prop in ordered)
|
|
||||||
{
|
{
|
||||||
if (prop.ArrayProperty.Index == last)
|
if (prop.ArrayProperty.Index == last)
|
||||||
|
// Don't write the same index twice
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
while (prop.ArrayProperty.Index != last + 1)
|
while (prop.ArrayProperty.Index != last + 1)
|
||||||
@@ -77,20 +65,21 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
}
|
}
|
||||||
|
|
||||||
JsonSerializerOptions? typeOptions = null;
|
JsonSerializerOptions? typeOptions = null;
|
||||||
if (prop.JsonConverterType != null)
|
if (prop.JsonConverter != null)
|
||||||
{
|
{
|
||||||
var converter = (JsonConverter)Activator.CreateInstance(prop.JsonConverterType)!;
|
typeOptions = new JsonSerializerOptions
|
||||||
typeOptions = new JsonSerializerOptions();
|
{
|
||||||
typeOptions.Converters.Clear();
|
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||||
typeOptions.Converters.Add(converter);
|
PropertyNameCaseInsensitive = false,
|
||||||
|
TypeInfoResolver = options.TypeInfoResolver,
|
||||||
|
};
|
||||||
|
typeOptions.Converters.Add(prop.JsonConverter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prop.JsonConverterType == null && IsSimple(prop.PropertyInfo.PropertyType))
|
if (prop.JsonConverter == null && IsSimple(prop.PropertyInfo.PropertyType))
|
||||||
{
|
{
|
||||||
if (prop.TargetType == typeof(string))
|
if (prop.TargetType == typeof(string))
|
||||||
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
|
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
|
||||||
else if(prop.TargetType.IsEnum)
|
|
||||||
writer.WriteStringValue(EnumConverter.GetString(objValue));
|
|
||||||
else if (prop.TargetType == typeof(bool))
|
else if (prop.TargetType == typeof(bool))
|
||||||
writer.WriteBooleanValue((bool)objValue);
|
writer.WriteBooleanValue((bool)objValue);
|
||||||
else
|
else
|
||||||
@@ -101,128 +90,157 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
JsonSerializer.Serialize(writer, objValue, typeOptions ?? options);
|
JsonSerializer.Serialize(writer, objValue, typeOptions ?? options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.WriteEndArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
writer.WriteEndArray();
|
||||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
}
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
var result = Activator.CreateInstance(typeToConvert)!;
|
/// <inheritdoc />
|
||||||
return (T)ParseObject(ref reader, result, typeToConvert, options);
|
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
}
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
|
return default;
|
||||||
|
|
||||||
private static bool IsSimple(Type type)
|
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 CeDeserializationException("Not an array");
|
||||||
|
|
||||||
|
|
||||||
|
if (_typePropertyInfo == null)
|
||||||
|
_typePropertyInfo = CacheTypeAttributes();
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
|
while (reader.Read())
|
||||||
{
|
{
|
||||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
if (reader.TokenType == JsonTokenType.EndArray)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if(!_typePropertyInfo.TryGetValue(index, out var indexAttributes))
|
||||||
{
|
{
|
||||||
// nullable type, check if the nested type is simple.
|
index++;
|
||||||
return IsSimple(type.GetGenericArguments()[0]);
|
continue;
|
||||||
}
|
|
||||||
return type.IsPrimitive
|
|
||||||
|| type.IsEnum
|
|
||||||
|| type == typeof(string)
|
|
||||||
|| type == typeof(decimal);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<ArrayPropertyInfo> CacheTypeAttributes(Type type)
|
|
||||||
{
|
|
||||||
var attributes = new List<ArrayPropertyInfo>();
|
|
||||||
var properties = type.GetProperties();
|
|
||||||
foreach (var property in properties)
|
|
||||||
{
|
|
||||||
var att = property.GetCustomAttribute<ArrayPropertyAttribute>();
|
|
||||||
if (att == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
attributes.Add(new ArrayPropertyInfo
|
|
||||||
{
|
|
||||||
ArrayProperty = att,
|
|
||||||
PropertyInfo = property,
|
|
||||||
DefaultDeserialization = property.GetCustomAttribute<JsonConversionAttribute>() != null,
|
|
||||||
JsonConverterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? property.PropertyType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType,
|
|
||||||
TargetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_typeAttributesCache.TryAdd(type, attributes);
|
foreach (var attribute in indexAttributes)
|
||||||
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)
|
var targetType = attribute.TargetType;
|
||||||
break;
|
object? value = null;
|
||||||
|
if (attribute.JsonConverter != null)
|
||||||
var indexAttributes = attributes.Where(a => a.ArrayProperty.Index == index);
|
|
||||||
if (!indexAttributes.Any())
|
|
||||||
{
|
{
|
||||||
index++;
|
if (attribute.JsonSerializerOptions == null)
|
||||||
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))
|
attribute.JsonSerializerOptions = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
var converter = (JsonConverter)Activator.CreateInstance(attribute.JsonConverterType)!;
|
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||||
newOptions = new JsonSerializerOptions
|
PropertyNameCaseInsensitive = false,
|
||||||
{
|
Converters = { attribute.JsonConverter },
|
||||||
NumberHandling = SerializerOptions.WithConverters.NumberHandling,
|
TypeInfoResolver = options.TypeInfoResolver,
|
||||||
PropertyNameCaseInsensitive = SerializerOptions.WithConverters.PropertyNameCaseInsensitive,
|
|
||||||
Converters = { converter },
|
|
||||||
};
|
|
||||||
_converterOptionsCache.TryAdd(attribute.JsonConverterType, newOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
value = JsonDocument.ParseValue(ref reader).Deserialize(attribute.PropertyInfo.PropertyType, newOptions);
|
|
||||||
}
|
|
||||||
else if (attribute.DefaultDeserialization)
|
|
||||||
{
|
|
||||||
// Use default deserialization
|
|
||||||
value = JsonDocument.ParseValue(ref reader).Deserialize(attribute.PropertyInfo.PropertyType, SerializerOptions.WithConverters);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
value = reader.TokenType switch
|
|
||||||
{
|
|
||||||
JsonTokenType.Null => null,
|
|
||||||
JsonTokenType.False => false,
|
|
||||||
JsonTokenType.True => true,
|
|
||||||
JsonTokenType.String => reader.GetString(),
|
|
||||||
JsonTokenType.Number => reader.GetDecimal(),
|
|
||||||
JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options),
|
|
||||||
_ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetType.IsAssignableFrom(value?.GetType()))
|
var doc = JsonDocument.ParseValue(ref reader);
|
||||||
attribute.PropertyInfo.SetValue(result, value);
|
value = doc.Deserialize(attribute.PropertyInfo.PropertyType, attribute.JsonSerializerOptions);
|
||||||
else
|
}
|
||||||
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
|
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 CeDeserializationException($"Array deserialization of type {reader.TokenType} not supported"),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
index++;
|
if (targetType.IsAssignableFrom(value?.GetType()))
|
||||||
|
attribute.PropertyInfo.SetValue(result, value);
|
||||||
|
else
|
||||||
|
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
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 SortedDictionary<int, List<ArrayPropertyInfo>> CacheTypeAttributes()
|
||||||
|
#else
|
||||||
|
private static SortedDictionary<int, List<ArrayPropertyInfo>> CacheTypeAttributes()
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
var result = new SortedDictionary<int, 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;
|
||||||
|
if (!result.TryGetValue(att.Index, out var indexList))
|
||||||
|
{
|
||||||
|
indexList = new List<ArrayPropertyInfo>();
|
||||||
|
result[att.Index] = indexList;
|
||||||
|
}
|
||||||
|
|
||||||
|
indexList.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 result;
|
||||||
|
}
|
||||||
|
|
||||||
|
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; } = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Diagnostics;
|
using System;
|
||||||
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;
|
||||||
@@ -20,59 +20,15 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
Type converterType = typeof(BoolConverterInner<>).MakeGenericType(typeToConvert);
|
return typeToConvert == typeof(bool) ? new BoolConverterInner() : new BoolConverterInnerNullable();
|
||||||
return (JsonConverter)Activator.CreateInstance(converterType)!;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class BoolConverterInner<T> : JsonConverter<T>
|
private class BoolConverterInnerNullable : JsonConverter<bool?>
|
||||||
{
|
{
|
||||||
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override bool? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
=> (T)((object?)ReadBool(ref reader, typeToConvert, options) ?? default(T))!;
|
=> ReadBool(ref reader, typeToConvert, options);
|
||||||
|
|
||||||
public bool? ReadBool(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, bool? value, 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)
|
if (value is bool boolVal)
|
||||||
writer.WriteBooleanValue(boolVal);
|
writer.WriteBooleanValue(boolVal);
|
||||||
@@ -81,5 +37,59 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class BoolConverterInner : JsonConverter<bool>
|
||||||
|
{
|
||||||
|
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
=> ReadBool(ref reader, typeToConvert, options) ?? false;
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteBooleanValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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))
|
||||||
|
LibraryHelpers.StaticLogger?.LogWarning("Received null or empty bool value, but property type is not a nullable bool. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name);
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,33 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
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>
|
/// <summary>
|
||||||
/// Converter for comma seperated enum values
|
/// Converter for comma separated enum values
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CommaSplitEnumConverter<T> : JsonConverter<IEnumerable<T>> where T : Enum
|
#if NET5_0_OR_GREATER
|
||||||
|
public class CommaSplitEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T> : JsonConverter<T[]> where T : struct, Enum
|
||||||
|
#else
|
||||||
|
public class CommaSplitEnumConverter<T> : JsonConverter<T[]> where T : struct, Enum
|
||||||
|
#endif
|
||||||
|
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override IEnumerable<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
return (reader.GetString()?.Split(',').Select(x => EnumConverter.ParseString<T>(x)).ToArray() ?? new T[0])!;
|
var str = reader.GetString();
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return str!.Split(',').Select(x => (T)EnumConverter.ParseString<T>(x)!).ToArray() ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Write(Utf8JsonWriter writer, IEnumerable<T> value, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x))));
|
writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x))));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Diagnostics;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -14,8 +14,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
{
|
{
|
||||||
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
|
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
|
||||||
private const double _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000d;
|
private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000m;
|
||||||
private const double _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000d / 1000;
|
private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool CanConvert(Type typeToConvert)
|
public override bool CanConvert(Type typeToConvert)
|
||||||
@@ -26,93 +26,107 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
Type converterType = typeof(DateTimeConverterInner<>).MakeGenericType(typeToConvert);
|
return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner() : new NullableDateTimeConverterInner();
|
||||||
return (JsonConverter)Activator.CreateInstance(converterType)!;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DateTimeConverterInner<T> : JsonConverter<T>
|
private class NullableDateTimeConverterInner : JsonConverter<DateTime?>
|
||||||
{
|
{
|
||||||
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
=> (T)((object?)ReadDateTime(ref reader, typeToConvert, options) ?? default(T))!;
|
=> ReadDateTime(ref reader, typeToConvert, options);
|
||||||
|
|
||||||
private DateTime? ReadDateTime(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, DateTime? value, 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)
|
if (value == null)
|
||||||
{
|
{
|
||||||
writer.WriteNullValue();
|
writer.WriteNullValue();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (value.Value == default)
|
||||||
|
writer.WriteStringValue(default(DateTime));
|
||||||
else
|
else
|
||||||
|
writer.WriteNumberValue((long)Math.Round((value.Value - new DateTime(1970, 1, 1)).TotalMilliseconds));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DateTimeConverterInner : JsonConverter<DateTime>
|
||||||
|
{
|
||||||
|
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
=> ReadDateTime(ref reader, typeToConvert, options) ?? default;
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
var dtValue = value;
|
||||||
|
if (dtValue == default)
|
||||||
|
writer.WriteStringValue(default(DateTime));
|
||||||
|
else
|
||||||
|
writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTime? ReadDateTime(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
|
{
|
||||||
|
if (typeToConvert == typeof(DateTime))
|
||||||
|
LibraryHelpers.StaticLogger?.LogWarning("DateTime value of null, but property is not nullable. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader.TokenType is JsonTokenType.Number)
|
||||||
|
{
|
||||||
|
var decValue = reader.GetDecimal();
|
||||||
|
if (decValue == 0 || decValue < 0)
|
||||||
|
return default;
|
||||||
|
|
||||||
|
return ParseFromDecimal(decValue);
|
||||||
|
}
|
||||||
|
else if (reader.TokenType is JsonTokenType.String)
|
||||||
|
{
|
||||||
|
var stringValue = reader.GetString();
|
||||||
|
if (string.IsNullOrWhiteSpace(stringValue)
|
||||||
|
|| stringValue!.Equals("-1", StringComparison.Ordinal)
|
||||||
|
|| stringValue!.Equals("0001-01-01T00:00:00Z", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| decimal.TryParse(stringValue, out var decVal) && decVal == 0)
|
||||||
{
|
{
|
||||||
var dtValue = (DateTime)(object)value;
|
return default;
|
||||||
if (dtValue == default)
|
|
||||||
writer.WriteStringValue(default(DateTime));
|
|
||||||
else
|
|
||||||
writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ParseFromString(stringValue!, options.TypeInfoResolver?.GetType()?.Name);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return reader.GetDateTime();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse a long value to datetime
|
/// Parse a double value to datetime
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="longValue"></param>
|
public static DateTime ParseFromDouble(double value)
|
||||||
/// <returns></returns>
|
=> ParseFromDecimal((decimal)value);
|
||||||
public static DateTime ParseFromDouble(double longValue)
|
|
||||||
{
|
|
||||||
if (longValue < 19999999999)
|
|
||||||
return ConvertFromSeconds(longValue);
|
|
||||||
if (longValue < 19999999999999)
|
|
||||||
return ConvertFromMilliseconds(longValue);
|
|
||||||
if (longValue < 19999999999999999)
|
|
||||||
return ConvertFromMicroseconds(longValue);
|
|
||||||
|
|
||||||
return ConvertFromNanoseconds(longValue);
|
/// <summary>
|
||||||
|
/// Parse a decimal value to datetime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ParseFromDecimal(decimal value)
|
||||||
|
{
|
||||||
|
if (value < 19999999999)
|
||||||
|
return ConvertFromSeconds(value);
|
||||||
|
if (value < 19999999999999)
|
||||||
|
return ConvertFromMilliseconds(value);
|
||||||
|
if (value < 19999999999999999)
|
||||||
|
return ConvertFromMicroseconds(value);
|
||||||
|
|
||||||
|
return ConvertFromNanoseconds(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse a string value to datetime
|
/// Parse a string value to datetime
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="stringValue"></param>
|
public static DateTime ParseFromString(string stringValue, string? resolverName)
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ParseFromString(string stringValue)
|
|
||||||
{
|
{
|
||||||
if (stringValue!.Length == 12 && stringValue.StartsWith("202"))
|
if (stringValue!.Length == 12 && stringValue.StartsWith("202", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// Parse 202303261200 format
|
// Parse 202303261200 format
|
||||||
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
||||||
@@ -121,7 +135,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|
||||||
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
|
|| !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);
|
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
|
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
|
||||||
@@ -134,7 +148,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
||||||
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
|
|| !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);
|
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
@@ -147,25 +161,25 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|
||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
|
|| !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);
|
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
if (decimal.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var decimalValue))
|
||||||
{
|
{
|
||||||
// Parse 1637745563.000 format
|
// Parse 1637745563.000 format
|
||||||
if (doubleValue <= 0)
|
if (decimalValue <= 0)
|
||||||
return default;
|
return default;
|
||||||
if (doubleValue < 19999999999)
|
if (decimalValue < 19999999999)
|
||||||
return ConvertFromSeconds(doubleValue);
|
return ConvertFromSeconds(decimalValue);
|
||||||
if (doubleValue < 19999999999999)
|
if (decimalValue < 19999999999999)
|
||||||
return ConvertFromMilliseconds((long)doubleValue);
|
return ConvertFromMilliseconds(decimalValue);
|
||||||
if (doubleValue < 19999999999999999)
|
if (decimalValue < 19999999999999999)
|
||||||
return ConvertFromMicroseconds((long)doubleValue);
|
return ConvertFromMicroseconds(decimalValue);
|
||||||
|
|
||||||
return ConvertFromNanoseconds((long)doubleValue);
|
return ConvertFromNanoseconds(decimalValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stringValue.Length == 10)
|
if (stringValue.Length == 10)
|
||||||
@@ -176,7 +190,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
|| !int.TryParse(values[1], out var month)
|
|| !int.TryParse(values[1], out var month)
|
||||||
|| !int.TryParse(values[2], out var day))
|
|| !int.TryParse(values[2], out var day))
|
||||||
{
|
{
|
||||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,54 +203,70 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="seconds"></param>
|
public static DateTime ConvertFromSeconds(decimal seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ConvertFromSeconds(double seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="milliseconds"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ConvertFromMilliseconds(double milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="microseconds"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ConvertFromMicroseconds(double microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nanoseconds"></param>
|
public static DateTime ConvertFromSeconds(double seconds) => ConvertFromSeconds((decimal)seconds);
|
||||||
/// <returns></returns>
|
/// <summary>
|
||||||
public static DateTime ConvertFromNanoseconds(double nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromSeconds(long seconds) => ConvertFromSeconds((decimal)seconds);
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromMilliseconds(decimal milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromMilliseconds(double milliseconds) => ConvertFromMilliseconds((decimal)milliseconds);
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromMilliseconds(long milliseconds) => ConvertFromMilliseconds((decimal)milliseconds);
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromMicroseconds(decimal microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromMicroseconds(double microseconds) => ConvertFromMicroseconds((decimal)microseconds);
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromMicroseconds(long microseconds) => ConvertFromMicroseconds((decimal)microseconds);
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromNanoseconds(decimal nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromNanoseconds(double nanoseconds) => ConvertFromNanoseconds((decimal)nanoseconds);
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromNanoseconds(long nanoseconds) => ConvertFromNanoseconds((decimal)nanoseconds);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
|
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("time")]
|
[return: NotNullIfNotNull("time")]
|
||||||
public static long? ConvertToSeconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalSeconds);
|
public static long? ConvertToSeconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalSeconds);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert a DateTime value to milliseconds since epoch (01-01-1970) value
|
/// Convert a DateTime value to milliseconds since epoch (01-01-1970) value
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("time")]
|
[return: NotNullIfNotNull("time")]
|
||||||
public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds);
|
public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert a DateTime value to microseconds since epoch (01-01-1970) value
|
/// Convert a DateTime value to microseconds since epoch (01-01-1970) value
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("time")]
|
[return: NotNullIfNotNull("time")]
|
||||||
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond);
|
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
|
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("time")]
|
[return: NotNullIfNotNull("time")]
|
||||||
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
|
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
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;
|
||||||
|
|
||||||
@@ -19,22 +18,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
if (reader.TokenType == JsonTokenType.String)
|
if (reader.TokenType == JsonTokenType.String)
|
||||||
{
|
{
|
||||||
var value = reader.GetString();
|
var value = reader.GetString();
|
||||||
if (string.IsNullOrEmpty(value) || string.Equals("null", value, StringComparison.OrdinalIgnoreCase))
|
return ExchangeHelpers.ParseDecimal(value);
|
||||||
return null;
|
|
||||||
|
|
||||||
if (string.Equals("Infinity", value, StringComparison.Ordinal))
|
|
||||||
// Infinity returned by the server, default to max value
|
|
||||||
return decimal.MaxValue;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
catch(OverflowException)
|
|
||||||
{
|
|
||||||
// Value doesn't fit decimal, default to max value
|
|
||||||
return decimal.MaxValue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Converter mapping to an object but also handles when an empty array is send
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T"></typeparam>
|
|
||||||
public class EmptyArrayObjectConverter<T> : JsonConverter<T>
|
|
||||||
{
|
|
||||||
private static JsonSerializerOptions _defaultConverter = SerializerOptions.WithConverters;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override T? Read(
|
|
||||||
ref Utf8JsonReader reader,
|
|
||||||
Type typeToConvert,
|
|
||||||
JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
switch (reader.TokenType)
|
|
||||||
{
|
|
||||||
case JsonTokenType.StartArray:
|
|
||||||
_ = JsonSerializer.Deserialize<object[]>(ref reader, options);
|
|
||||||
return default;
|
|
||||||
case JsonTokenType.StartObject:
|
|
||||||
return JsonSerializer.Deserialize<T>(ref reader, _defaultConverter);
|
|
||||||
};
|
|
||||||
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
|
||||||
=> JsonSerializer.Serialize(writer, (object?)value, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
using CryptoExchange.Net.Attributes;
|
using CryptoExchange.Net.Attributes;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
#endif
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
@@ -11,127 +14,96 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Static EnumConverter methods
|
||||||
|
/// </summary>
|
||||||
|
public static class EnumConverter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get the enum value from a string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">String value</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
public static T? ParseString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string value) where T : struct, Enum
|
||||||
|
#else
|
||||||
|
public static T? ParseString<T>(string value) where T : struct, Enum
|
||||||
|
#endif
|
||||||
|
=> EnumConverter<T>.ParseString(value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="enumValue"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
public static string GetString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(T enumValue) where T : struct, Enum
|
||||||
|
#else
|
||||||
|
public static string GetString<T>(T enumValue) where T : struct, Enum
|
||||||
|
#endif
|
||||||
|
=> EnumConverter<T>.GetString(enumValue);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="enumValue"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[return: NotNullIfNotNull("enumValue")]
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
public static string? GetString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(T? enumValue) where T : struct, Enum
|
||||||
|
#else
|
||||||
|
public static string? GetString<T>(T? enumValue) where T : struct, Enum
|
||||||
|
#endif
|
||||||
|
=> EnumConverter<T>.GetString(enumValue);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
|
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class EnumConverter : JsonConverterFactory
|
#if NET5_0_OR_GREATER
|
||||||
|
public class EnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>
|
||||||
|
#else
|
||||||
|
public class EnumConverter<T>
|
||||||
|
#endif
|
||||||
|
: JsonConverter<T>, INullableConverterFactory where T : struct, Enum
|
||||||
{
|
{
|
||||||
private bool _warnOnMissingEntry = true;
|
class EnumMapping
|
||||||
private bool _writeAsInt;
|
|
||||||
private static readonly ConcurrentDictionary<Type, List<KeyValuePair<object, string>>> _mapping = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// </summary>
|
|
||||||
public EnumConverter() { }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="writeAsInt"></param>
|
|
||||||
/// <param name="warnOnMissingEntry"></param>
|
|
||||||
public EnumConverter(bool writeAsInt, bool warnOnMissingEntry)
|
|
||||||
{
|
{
|
||||||
_writeAsInt = writeAsInt;
|
public T Value { get; set; }
|
||||||
_warnOnMissingEntry = warnOnMissingEntry;
|
public string StringValue { get; set; }
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
public EnumMapping(T value, string stringValue)
|
||||||
public override bool CanConvert(Type typeToConvert)
|
|
||||||
{
|
|
||||||
return typeToConvert.IsEnum || Nullable.GetUnderlyingType(typeToConvert)?.IsEnum == true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
JsonConverter converter = (JsonConverter)Activator.CreateInstance(
|
|
||||||
typeof(EnumConverterInner<>).MakeGenericType(
|
|
||||||
new Type[] { typeToConvert }),
|
|
||||||
BindingFlags.Instance | BindingFlags.Public,
|
|
||||||
binder: null,
|
|
||||||
args: new object[] { _writeAsInt, _warnOnMissingEntry },
|
|
||||||
culture: null)!;
|
|
||||||
|
|
||||||
return converter;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<KeyValuePair<object, string>> AddMapping(Type objectType)
|
|
||||||
{
|
|
||||||
var mapping = new List<KeyValuePair<object, string>>();
|
|
||||||
var enumMembers = objectType.GetMembers();
|
|
||||||
foreach (var member in enumMembers)
|
|
||||||
{
|
{
|
||||||
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
|
Value = value;
|
||||||
foreach (MapAttribute attribute in maps)
|
StringValue = stringValue;
|
||||||
{
|
|
||||||
foreach (var value in attribute.Values)
|
|
||||||
mapping.Add(new KeyValuePair<object, string>(Enum.Parse(objectType, member.Name), value));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_mapping.TryAdd(objectType, mapping);
|
|
||||||
return mapping;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class EnumConverterInner<T> : JsonConverter<T>
|
#if NET8_0_OR_GREATER
|
||||||
|
private static FrozenSet<EnumMapping>? _mappingToEnum = null;
|
||||||
|
private static FrozenDictionary<T, string>? _mappingToString = null;
|
||||||
|
#else
|
||||||
|
private static List<EnumMapping>? _mappingToEnum = null;
|
||||||
|
private static Dictionary<T, string>? _mappingToString = null;
|
||||||
|
#endif
|
||||||
|
private NullableEnumConverter? _nullableEnumConverter = null;
|
||||||
|
|
||||||
|
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;
|
|
||||||
|
|
||||||
public EnumConverterInner(bool writeAsInt, bool warnOnMissingEntry)
|
public NullableEnumConverter(EnumConverter<T> enumConverter)
|
||||||
{
|
{
|
||||||
_warnOnMissingEntry = warnOnMissingEntry;
|
_enumConverter = enumConverter;
|
||||||
_writeAsInt = writeAsInt;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
var enumType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert;
|
return _enumConverter.ReadNullable(ref reader, typeToConvert, options, out var isEmptyString);
|
||||||
if (!_mapping.TryGetValue(enumType, out var mapping))
|
|
||||||
mapping = AddMapping(enumType);
|
|
||||||
|
|
||||||
var stringValue = reader.TokenType switch
|
|
||||||
{
|
|
||||||
JsonTokenType.String => reader.GetString(),
|
|
||||||
JsonTokenType.Number => reader.GetInt16().ToString(),
|
|
||||||
JsonTokenType.True => reader.GetBoolean().ToString(),
|
|
||||||
JsonTokenType.False => reader.GetBoolean().ToString(),
|
|
||||||
JsonTokenType.Null => null,
|
|
||||||
_ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(stringValue))
|
|
||||||
{
|
|
||||||
// Received null value
|
|
||||||
var emptyResult = GetDefaultValue(typeToConvert, enumType);
|
|
||||||
if (emptyResult != null)
|
|
||||||
// If the property we're parsing to isn't nullable there isn't a correct way to return this as null will either throw an exception (.net framework) or the default enum value (dotnet core).
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null enum value, but property type is not a nullable enum. EnumType: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo");
|
|
||||||
|
|
||||||
return (T?)emptyResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!GetValue(enumType, mapping, stringValue!, out var result))
|
|
||||||
{
|
|
||||||
var defaultValue = GetDefaultValue(typeToConvert, enumType);
|
|
||||||
if (string.IsNullOrWhiteSpace(stringValue))
|
|
||||||
{
|
|
||||||
if (defaultValue != null)
|
|
||||||
// We received an empty string and have no mapping for it, and the property isn't nullable
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received empty string as enum value, but property type is not a nullable enum. EnumType: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// We received an enum value but weren't able to parse it.
|
|
||||||
if (_warnOnMissingEntry)
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {enumType.Name}, Value: {stringValue}, Known values: {string.Join(", ", mapping.Select(m => m.Value))}. If you think {stringValue} should added please open an issue on the Github repo");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (T?)defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (T?)result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
if (value == null)
|
if (value == null)
|
||||||
{
|
{
|
||||||
@@ -139,107 +111,233 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!_writeAsInt)
|
_enumConverter.Write(writer, value.Value, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString);
|
||||||
|
if (t == null)
|
||||||
|
{
|
||||||
|
if (isEmptyString && !_unknownValuesWarned.Contains(null))
|
||||||
|
{
|
||||||
|
// We received an empty string and have no mapping for it, and the property isn't nullable
|
||||||
|
LibraryHelpers.StaticLogger?.LogWarning($"Received null or empty enum value, but property type is not a nullable enum. EnumType: {typeof(T).FullName}. If you think {typeof(T).FullName} should be nullable please open an issue on the Github repo");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new T(); // return default value
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return t.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString)
|
||||||
|
{
|
||||||
|
isEmptyString = false;
|
||||||
|
var enumType = typeof(T);
|
||||||
|
if (_mappingToEnum == null)
|
||||||
|
CreateMapping();
|
||||||
|
|
||||||
|
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 (stringValue is null)
|
||||||
|
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))
|
||||||
{
|
{
|
||||||
var stringValue = GetString(value.GetType(), value);
|
_unknownValuesWarned.Add(stringValue!);
|
||||||
writer.WriteStringValue(stringValue);
|
LibraryHelpers.StaticLogger?.LogWarning($"Cannot map enum value. EnumType: {enumType.FullName}, Value: {stringValue}, Known values: {string.Join(", ", _mappingToEnum!.Select(m => m.Value))}. If you think {stringValue} should added please open an issue on the Github repo");
|
||||||
}
|
}
|
||||||
else
|
}
|
||||||
|
|
||||||
|
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 (_mappingToEnum != null)
|
||||||
|
{
|
||||||
|
EnumMapping? mapping = null;
|
||||||
|
// Try match on full equals
|
||||||
|
foreach (var item in _mappingToEnum)
|
||||||
|
{
|
||||||
|
if (item.StringValue.Equals(value, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
writer.WriteNumberValue((int)Convert.ChangeType(value, typeof(int)));
|
mapping = item;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found, try matching ignoring case
|
||||||
|
if (mapping == null)
|
||||||
|
{
|
||||||
|
foreach (var item in _mappingToEnum)
|
||||||
|
{
|
||||||
|
if (item.StringValue.Equals(value, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
mapping = item;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping != null)
|
||||||
|
{
|
||||||
|
result = mapping.Value;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
// An empty/null value will always fail when parsing, so just return here
|
||||||
|
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 void CreateMapping()
|
||||||
|
{
|
||||||
|
var mappingToEnum = new List<EnumMapping>();
|
||||||
|
var mappingToString = new Dictionary<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)
|
||||||
|
{
|
||||||
|
var enumVal = (T)Enum.Parse(enumType, member.Name);
|
||||||
|
mappingToEnum.Add(new EnumMapping(enumVal, value));
|
||||||
|
if (!mappingToString.ContainsKey(enumVal))
|
||||||
|
mappingToString.Add(enumVal, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static object? GetDefaultValue(Type objectType, Type enumType)
|
#if NET8_0_OR_GREATER
|
||||||
{
|
_mappingToEnum = mappingToEnum.ToFrozenSet();
|
||||||
if (Nullable.GetUnderlyingType(objectType) != null)
|
_mappingToString = mappingToString.ToFrozenDictionary();
|
||||||
return null;
|
#else
|
||||||
|
_mappingToEnum = mappingToEnum;
|
||||||
return Activator.CreateInstance(enumType); // return default value
|
_mappingToString = mappingToString;
|
||||||
}
|
#endif
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (objectType.IsDefined(typeof(FlagsAttribute)))
|
|
||||||
{
|
|
||||||
var intValue = int.Parse(value);
|
|
||||||
result = Enum.ToObject(objectType, intValue);
|
|
||||||
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>
|
/// <summary>
|
||||||
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T"></typeparam>
|
|
||||||
/// <param name="enumValue"></param>
|
/// <param name="enumValue"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[return: NotNullIfNotNull("enumValue")]
|
[return: NotNullIfNotNull("enumValue")]
|
||||||
public static string? GetString<T>(T enumValue) => GetString(typeof(T), enumValue);
|
public static string? GetString(T? enumValue)
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="objectType"></param>
|
|
||||||
/// <param name="enumValue"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("enumValue")]
|
|
||||||
public static string? GetString(Type objectType, object? enumValue)
|
|
||||||
{
|
{
|
||||||
objectType = Nullable.GetUnderlyingType(objectType) ?? objectType;
|
if (_mappingToString == null)
|
||||||
|
CreateMapping();
|
||||||
|
|
||||||
if (!_mapping.TryGetValue(objectType, out var mapping))
|
return enumValue == null ? null : (_mappingToString!.TryGetValue(enumValue.Value, out var str) ? str : enumValue.ToString());
|
||||||
mapping = AddMapping(objectType);
|
|
||||||
|
|
||||||
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the enum value from a string
|
/// Get the enum value from a string
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">Enum type</typeparam>
|
|
||||||
/// <param name="value">String value</param>
|
/// <param name="value">String value</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static T? ParseString<T>(string value) where T : Enum
|
public static T? ParseString(string value)
|
||||||
{
|
{
|
||||||
var type = typeof(T);
|
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||||
if (!_mapping.TryGetValue(type, out var enumMapping))
|
if (_mappingToEnum == null)
|
||||||
enumMapping = AddMapping(type);
|
CreateMapping();
|
||||||
|
|
||||||
var mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
|
EnumMapping? mapping = null;
|
||||||
if (mapping.Equals(default(KeyValuePair<object, string>)))
|
// Try match on full equals
|
||||||
mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
foreach(var item in _mappingToEnum!)
|
||||||
|
|
||||||
if (!mapping.Equals(default(KeyValuePair<object, string>)))
|
|
||||||
{
|
{
|
||||||
return (T)mapping.Key;
|
if (item.StringValue.Equals(value, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
mapping = item;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If not found, try matching ignoring case
|
||||||
|
if (mapping == null)
|
||||||
|
{
|
||||||
|
foreach (var item in _mappingToEnum)
|
||||||
|
{
|
||||||
|
if (item.StringValue.Equals(value, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
mapping = item;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping != null)
|
||||||
|
return mapping.Value;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// If no explicit mapping is found try to parse string
|
// If no explicit mapping is found try to parse string
|
||||||
@@ -250,5 +348,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public JsonConverter CreateNullableConverter()
|
||||||
|
{
|
||||||
|
_nullableEnumConverter ??= new NullableEnumConverter(this);
|
||||||
|
return _nullableEnumConverter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
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,9 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
|
internal interface INullableConverterFactory
|
||||||
|
{
|
||||||
|
JsonConverter CreateNullableConverter();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Attribute for allowing specifying a JsonConverter with constructor parameters
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(AttributeTargets.Property)]
|
|
||||||
public class JsonConverterCtorAttribute : JsonConverterAttribute
|
|
||||||
{
|
|
||||||
private readonly object[] _parameters;
|
|
||||||
private readonly Type _type;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public JsonConverterCtorAttribute(Type type, params object[] parameters)
|
|
||||||
{
|
|
||||||
_type = type;
|
|
||||||
_parameters = parameters;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override JsonConverter CreateConverter(Type typeToConvert)
|
|
||||||
{
|
|
||||||
return (JsonConverter)Activator.CreateInstance(_type, _parameters)!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Errors;
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// JSON REST message handler
|
||||||
|
/// </summary>
|
||||||
|
public abstract class JsonRestMessageHandler : IRestMessageHandler
|
||||||
|
{
|
||||||
|
private static MediaTypeWithQualityHeaderValue _acceptJsonContent = new MediaTypeWithQualityHeaderValue(Constants.JsonContentHeader);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Empty rate limit error
|
||||||
|
/// </summary>
|
||||||
|
protected static readonly ServerRateLimitError _emptyRateLimitError = new ServerRateLimitError();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual bool RequiresSeekableStream => false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The serializer options to use
|
||||||
|
/// </summary>
|
||||||
|
public abstract JsonSerializerOptions Options { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public MediaTypeWithQualityHeaderValue AcceptHeader => _acceptJsonContent;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual ValueTask<ServerRateLimitError> ParseErrorRateLimitResponse(
|
||||||
|
int httpStatusCode,
|
||||||
|
HttpResponseHeaders responseHeaders,
|
||||||
|
Stream responseStream)
|
||||||
|
{
|
||||||
|
// Handle retry after header
|
||||||
|
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
if (retryAfterHeader.Value?.Any() != true)
|
||||||
|
return new ValueTask<ServerRateLimitError>(_emptyRateLimitError);
|
||||||
|
|
||||||
|
var value = retryAfterHeader.Value.First();
|
||||||
|
if (int.TryParse(value, out var seconds))
|
||||||
|
return new ValueTask<ServerRateLimitError>(new ServerRateLimitError() { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) });
|
||||||
|
|
||||||
|
if (DateTime.TryParse(value, out var datetime))
|
||||||
|
return new ValueTask<ServerRateLimitError>(new ServerRateLimitError() { RetryAfter = datetime });
|
||||||
|
|
||||||
|
return new ValueTask<ServerRateLimitError>(_emptyRateLimitError);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public abstract ValueTask<Error> ParseErrorResponse(
|
||||||
|
int httpStatusCode,
|
||||||
|
HttpResponseHeaders responseHeaders,
|
||||||
|
Stream responseStream);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual ValueTask<Error?> CheckForErrorResponse(
|
||||||
|
RequestDefinition request,
|
||||||
|
HttpResponseHeaders responseHeaders,
|
||||||
|
Stream responseStream) => new ValueTask<Error?>((Error?)null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read the response into a JsonDocument object
|
||||||
|
/// </summary>
|
||||||
|
protected virtual async ValueTask<(Error?, JsonDocument?)> GetJsonDocument(Stream stream)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var document = await JsonDocument.ParseAsync(stream).ConfigureAwait(false);
|
||||||
|
return (null, document);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return (new ServerError(new ErrorInfo(ErrorType.DeserializationFailed, false, "Deserialization failed, invalid JSON"), ex), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
#endif
|
||||||
|
public async ValueTask<(T? Result, Error? Error)> TryDeserializeAsync<T>(Stream responseStream, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||||
|
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||||
|
var result = await JsonSerializer.DeserializeAsync<T>(responseStream, Options)!.ConfigureAwait(false)!;
|
||||||
|
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||||
|
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||||
|
return (result, null);
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
||||||
|
return (default, new DeserializeError(info, ex));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return (default, new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual Error? CheckDeserializedResponse<T>(HttpResponseHeaders responseHeaders, T result) => null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+348
@@ -0,0 +1,348 @@
|
|||||||
|
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// JSON WebSocket message handler, sequentially read the JSON and looks for specific predefined fields to identify the message
|
||||||
|
/// </summary>
|
||||||
|
public abstract class JsonSocketMessageHandler : ISocketMessageHandler
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The serializer options to use
|
||||||
|
/// </summary>
|
||||||
|
public abstract JsonSerializerOptions Options { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Message evaluators
|
||||||
|
/// </summary>
|
||||||
|
protected abstract MessageTypeDefinition[] TypeEvaluators { get; }
|
||||||
|
|
||||||
|
private readonly SearchResult _searchResult = new();
|
||||||
|
|
||||||
|
private bool _hasArraySearches;
|
||||||
|
private bool _initialized;
|
||||||
|
private int _maxSearchDepth;
|
||||||
|
private MessageTypeDefinition? _topEvaluator;
|
||||||
|
private List<MessageEvalutorFieldReference>? _searchFields;
|
||||||
|
private Dictionary<Type, Func<object, string?>>? _baseTypeMapping;
|
||||||
|
private Dictionary<Type, Func<object, string?>>? _mapping;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a mapping of a specific object of a type to a specific topic
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Type to get topic for</typeparam>
|
||||||
|
/// <param name="mapping">The topic retrieve delegate</param>
|
||||||
|
protected void AddTopicMapping<T>(Func<T, string?> mapping)
|
||||||
|
{
|
||||||
|
_mapping ??= new Dictionary<Type, Func<object, string?>>();
|
||||||
|
_mapping.Add(typeof(T), x => mapping((T)x));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeConverter()
|
||||||
|
{
|
||||||
|
if (_initialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_maxSearchDepth = int.MinValue;
|
||||||
|
_searchFields = new List<MessageEvalutorFieldReference>();
|
||||||
|
foreach (var evaluator in TypeEvaluators)
|
||||||
|
{
|
||||||
|
_topEvaluator ??= evaluator;
|
||||||
|
foreach (var field in evaluator.Fields)
|
||||||
|
{
|
||||||
|
var overlapping = _searchFields.Where(otherField =>
|
||||||
|
{
|
||||||
|
if (field is PropertyFieldReference propRef
|
||||||
|
&& otherField.Field is PropertyFieldReference otherPropRef)
|
||||||
|
{
|
||||||
|
return field.Depth == otherPropRef.Depth && propRef.PropertyName.SequenceEqual(otherPropRef.PropertyName);
|
||||||
|
}
|
||||||
|
else if (field is ArrayFieldReference arrayRef
|
||||||
|
&& otherField.Field is ArrayFieldReference otherArrayPropRef)
|
||||||
|
{
|
||||||
|
return field.Depth == otherArrayPropRef.Depth && arrayRef.ArrayIndex == otherArrayPropRef.ArrayIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
if (overlapping.Any())
|
||||||
|
{
|
||||||
|
foreach (var overlap in overlapping)
|
||||||
|
overlap.OverlappingField = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MessageEvalutorFieldReference>? existingSameSearchField = new();
|
||||||
|
if (field is ArrayFieldReference arrayField)
|
||||||
|
{
|
||||||
|
_hasArraySearches = true;
|
||||||
|
existingSameSearchField = _searchFields.Where(x =>
|
||||||
|
x.Field is ArrayFieldReference arrayFieldRef
|
||||||
|
&& arrayFieldRef.ArrayIndex == arrayField.ArrayIndex
|
||||||
|
&& arrayFieldRef.Depth == arrayField.Depth
|
||||||
|
&& arrayFieldRef.Constraint == null && arrayField.Constraint == null).ToList();
|
||||||
|
}
|
||||||
|
else if (field is PropertyFieldReference propField)
|
||||||
|
{
|
||||||
|
existingSameSearchField = _searchFields.Where(x =>
|
||||||
|
x.Field is PropertyFieldReference propFieldRef
|
||||||
|
&& propFieldRef.PropertyName.SequenceEqual(propField.PropertyName)
|
||||||
|
&& propFieldRef.Depth == propField.Depth
|
||||||
|
&& propFieldRef.Constraint == null && propFieldRef.Constraint == null).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach(var sameSearchField in existingSameSearchField)
|
||||||
|
{
|
||||||
|
if (sameSearchField.SkipReading == true
|
||||||
|
&& (evaluator.TypeIdentifierCallback != null || field.Constraint != null))
|
||||||
|
{
|
||||||
|
sameSearchField.SkipReading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evaluator.ForceIfFound)
|
||||||
|
{
|
||||||
|
if (evaluator.Fields.Length > 1 || sameSearchField.ForceEvaluator != null)
|
||||||
|
throw new Exception("Invalid config");
|
||||||
|
|
||||||
|
//sameSearchField.ForceEvaluator = evaluator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_searchFields.Add(new MessageEvalutorFieldReference(field)
|
||||||
|
{
|
||||||
|
SkipReading = evaluator.TypeIdentifierCallback == null && field.Constraint == null,
|
||||||
|
ForceEvaluator = !existingSameSearchField.Any() ? evaluator.ForceIfFound ? evaluator : null : null,
|
||||||
|
OverlappingField = overlapping.Any()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (field.Depth > _maxSearchDepth)
|
||||||
|
_maxSearchDepth = field.Depth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual string? GetTopicFilter(object deserializedObject)
|
||||||
|
{
|
||||||
|
if (_mapping == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Cache the found type for future
|
||||||
|
var currentType = deserializedObject.GetType();
|
||||||
|
if (_baseTypeMapping != null)
|
||||||
|
{
|
||||||
|
if (_baseTypeMapping.TryGetValue(currentType, out var typeMapping))
|
||||||
|
return typeMapping(deserializedObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
var mappedBase = false;
|
||||||
|
while (currentType != null)
|
||||||
|
{
|
||||||
|
if (_mapping.TryGetValue(currentType, out var mapping))
|
||||||
|
{
|
||||||
|
if (mappedBase)
|
||||||
|
{
|
||||||
|
_baseTypeMapping ??= new Dictionary<Type, Func<object, string?>>();
|
||||||
|
_baseTypeMapping.Add(deserializedObject.GetType(), mapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapping(deserializedObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
mappedBase = true;
|
||||||
|
currentType = currentType.BaseType;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
|
||||||
|
{
|
||||||
|
InitializeConverter();
|
||||||
|
|
||||||
|
int? arrayIndex = null;
|
||||||
|
|
||||||
|
_searchResult.Clear();
|
||||||
|
var reader = new Utf8JsonReader(data);
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
if ((reader.TokenType == JsonTokenType.StartArray
|
||||||
|
|| reader.TokenType == JsonTokenType.StartObject)
|
||||||
|
&& reader.CurrentDepth == _maxSearchDepth)
|
||||||
|
{
|
||||||
|
// There is no field we need to search for on a depth deeper than this, skip
|
||||||
|
reader.Skip();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader.TokenType == JsonTokenType.StartArray)
|
||||||
|
arrayIndex = -1;
|
||||||
|
else if (reader.TokenType == JsonTokenType.EndArray)
|
||||||
|
arrayIndex = null;
|
||||||
|
else if (arrayIndex != null)
|
||||||
|
arrayIndex++;
|
||||||
|
|
||||||
|
if (reader.TokenType == JsonTokenType.PropertyName
|
||||||
|
|| arrayIndex != null && _hasArraySearches)
|
||||||
|
{
|
||||||
|
bool written = false;
|
||||||
|
|
||||||
|
string? value = null;
|
||||||
|
byte[]? propName = null;
|
||||||
|
foreach (var field in _searchFields!)
|
||||||
|
{
|
||||||
|
if (field.Field.Depth != reader.CurrentDepth)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
bool readArrayValues = false;
|
||||||
|
if (field.Field is PropertyFieldReference propFieldRef)
|
||||||
|
{
|
||||||
|
if (propName == null)
|
||||||
|
{
|
||||||
|
if (reader.TokenType != JsonTokenType.PropertyName)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!reader.ValueTextEquals(propFieldRef.PropertyName))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
propName = propFieldRef.PropertyName;
|
||||||
|
readArrayValues = propFieldRef.ArrayValues;
|
||||||
|
reader.Read();
|
||||||
|
}
|
||||||
|
else if (!propFieldRef.PropertyName.SequenceEqual(propName))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (field.Field is ArrayFieldReference arrayFieldRef)
|
||||||
|
{
|
||||||
|
if (propName != null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (reader.TokenType == JsonTokenType.PropertyName)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (arrayFieldRef.ArrayIndex != arrayIndex)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!field.SkipReading)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
if (readArrayValues)
|
||||||
|
{
|
||||||
|
if (reader.TokenType != JsonTokenType.StartArray)
|
||||||
|
// error
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
reader.Read();// Read start array
|
||||||
|
bool first = true;
|
||||||
|
while(reader.TokenType != JsonTokenType.EndArray)
|
||||||
|
{
|
||||||
|
if (!first)
|
||||||
|
sb.Append(",");
|
||||||
|
|
||||||
|
first = false;
|
||||||
|
sb.Append(reader.GetString());
|
||||||
|
reader.Read();
|
||||||
|
}
|
||||||
|
|
||||||
|
value = first ? null : sb.ToString();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
switch (reader.TokenType)
|
||||||
|
{
|
||||||
|
case JsonTokenType.Number:
|
||||||
|
value = reader.GetDecimal().ToString();
|
||||||
|
break;
|
||||||
|
case JsonTokenType.String:
|
||||||
|
value = reader.GetString()!;
|
||||||
|
break;
|
||||||
|
case JsonTokenType.True:
|
||||||
|
case JsonTokenType.False:
|
||||||
|
value = reader.GetBoolean().ToString()!;
|
||||||
|
break;
|
||||||
|
case JsonTokenType.Null:
|
||||||
|
value = null;
|
||||||
|
break;
|
||||||
|
case JsonTokenType.StartObject:
|
||||||
|
case JsonTokenType.StartArray:
|
||||||
|
value = null;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.Field.Constraint != null
|
||||||
|
&& !field.Field.Constraint(value))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_searchResult.Write(field.Field, value);
|
||||||
|
|
||||||
|
if (field.ForceEvaluator != null)
|
||||||
|
{
|
||||||
|
if (field.ForceEvaluator.StaticIdentifier != null)
|
||||||
|
return field.ForceEvaluator.StaticIdentifier;
|
||||||
|
|
||||||
|
// Force the immediate return upon encountering this field
|
||||||
|
return field.ForceEvaluator.GetMessageType(_searchResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
written = true;
|
||||||
|
if (!field.OverlappingField)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!written)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (_topEvaluator!.Satisfied(_searchResult))
|
||||||
|
return _topEvaluator.GetMessageType(_searchResult);
|
||||||
|
|
||||||
|
if (_searchFields.Count == _searchResult.Count)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var evaluator in TypeEvaluators)
|
||||||
|
{
|
||||||
|
if (evaluator.Satisfied(_searchResult))
|
||||||
|
return evaluator.GetMessageType(_searchResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
#endif
|
||||||
|
public virtual object Deserialize(ReadOnlySpan<byte> data, Type type)
|
||||||
|
{
|
||||||
|
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||||
|
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||||
|
return JsonSerializer.Deserialize(data, type, Options)!;
|
||||||
|
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||||
|
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||||
|
using System;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// JSON WebSocket message handler, reads the json data info a JsonDocument after which the data can be inspected to identify the message
|
||||||
|
/// </summary>
|
||||||
|
public abstract class JsonSocketPreloadMessageHandler : ISocketMessageHandler
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The serializer options to use
|
||||||
|
/// </summary>
|
||||||
|
public abstract JsonSerializerOptions Options { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
|
||||||
|
{
|
||||||
|
var reader = new Utf8JsonReader(data);
|
||||||
|
var jsonDocument = JsonDocument.ParseValue(ref reader);
|
||||||
|
|
||||||
|
return GetTypeIdentifier(jsonDocument);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the message identifier for this document
|
||||||
|
/// </summary>
|
||||||
|
protected abstract string? GetTypeIdentifier(JsonDocument document);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get optional topic filter, for example a symbol name
|
||||||
|
/// </summary>
|
||||||
|
public virtual string? GetTopicFilter(object deserializedObject) => null;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual object Deserialize(ReadOnlySpan<byte> data, Type type)
|
||||||
|
{
|
||||||
|
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||||
|
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||||
|
return JsonSerializer.Deserialize(data, type, Options)!;
|
||||||
|
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||||
|
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the string value for a path, or an emtpy string if not found
|
||||||
|
/// </summary>
|
||||||
|
protected string StringOrEmpty(JsonDocument document, string path)
|
||||||
|
{
|
||||||
|
if (!document.RootElement.TryGetProperty(path, out var element))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
if (element.ValueKind == JsonValueKind.String)
|
||||||
|
return element.GetString() ?? string.Empty;
|
||||||
|
else if (element.ValueKind == JsonValueKind.Number)
|
||||||
|
return element.GetDecimal().ToString();
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||||
|
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converter for parsing object or array responses
|
||||||
|
/// </summary>
|
||||||
|
public class ObjectOrArrayConverter : JsonConverterFactory
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override bool CanConvert(Type typeToConvert) => true;
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
var type = typeof(InternalObjectOrArrayConverter<>).MakeGenericType(typeToConvert);
|
||||||
|
return (JsonConverter)Activator.CreateInstance(type)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class InternalObjectOrArrayConverter<T> : JsonConverter<T>
|
||||||
|
{
|
||||||
|
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.StartObject && !typeToConvert.IsArray)
|
||||||
|
{
|
||||||
|
// Object to object
|
||||||
|
return JsonDocument.ParseValue(ref reader).Deserialize<T>(options);
|
||||||
|
}
|
||||||
|
else if (reader.TokenType == JsonTokenType.StartArray && typeToConvert.IsArray)
|
||||||
|
{
|
||||||
|
// Array to array
|
||||||
|
return JsonDocument.ParseValue(ref reader).Deserialize<T>(options);
|
||||||
|
}
|
||||||
|
else if (reader.TokenType == JsonTokenType.StartArray)
|
||||||
|
{
|
||||||
|
// Array to object
|
||||||
|
JsonDocument.ParseValue(ref reader).Deserialize<T[]>(options);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Object to array
|
||||||
|
JsonDocument.ParseValue(ref reader);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
JsonSerializer.Serialize(writer, value, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,20 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Converter for values which contain a nested json value
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T"></typeparam>
|
|
||||||
public class ObjectStringConverter<T> : JsonConverter<T>
|
public class ObjectStringConverter<T> : JsonConverter<T>
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
#endif
|
||||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
@@ -20,10 +24,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
if (string.IsNullOrEmpty(value))
|
if (string.IsNullOrEmpty(value))
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T));
|
return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T), options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
#endif
|
||||||
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
if (value is null)
|
if (value is null)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Replace a value on a string property
|
/// Replace a value on a string property
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ReplaceConverter : JsonConverter<string>
|
public abstract class ReplaceConverter : JsonConverter<string>
|
||||||
{
|
{
|
||||||
private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets;
|
private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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,4 +1,5 @@
|
|||||||
using System.Text.Json;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
@@ -8,22 +9,39 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class SerializerOptions
|
public static class SerializerOptions
|
||||||
{
|
{
|
||||||
|
private static readonly ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions> _cache = new ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Json serializer settings which includes the EnumConverter, DateTimeConverter, BoolConverter and DecimalConverter
|
/// Get Json serializer settings which includes standard converters for DateTime, bool, enum and number types
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static JsonSerializerOptions WithConverters { get; } = new JsonSerializerOptions
|
public static JsonSerializerOptions WithConverters(JsonSerializerContext typeResolver, params JsonConverter[] additionalConverters)
|
||||||
{
|
{
|
||||||
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
if (!_cache.TryGetValue(typeResolver, out var options))
|
||||||
PropertyNameCaseInsensitive = false,
|
{
|
||||||
Converters =
|
options = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||||
|
PropertyNameCaseInsensitive = false,
|
||||||
|
Converters =
|
||||||
{
|
{
|
||||||
new DateTimeConverter(),
|
new DateTimeConverter(),
|
||||||
new EnumConverter(),
|
|
||||||
new BoolConverter(),
|
new BoolConverter(),
|
||||||
new DecimalConverter(),
|
new DecimalConverter(),
|
||||||
new IntConverter(),
|
new IntConverter(),
|
||||||
new LongConverter()
|
new LongConverter(),
|
||||||
}
|
new NullableEnumConverterFactory(typeResolver)
|
||||||
};
|
},
|
||||||
|
TypeInfoResolver = typeResolver,
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var converter in additionalConverters)
|
||||||
|
options.Converters.Add(converter);
|
||||||
|
|
||||||
|
options.TypeInfoResolver = typeResolver;
|
||||||
|
_cache.TryAdd(typeResolver, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
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,44 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
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.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -20,11 +20,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected JsonDocument? _document;
|
protected JsonDocument? _document;
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions _serializerOptions = SerializerOptions.WithConverters;
|
|
||||||
private readonly JsonSerializerOptions? _customSerializerOptions;
|
private readonly JsonSerializerOptions? _customSerializerOptions;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool IsJson { get; set; }
|
public bool IsValid { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public abstract bool OriginalDataAvailable { get; }
|
public abstract bool OriginalDataAvailable { get; }
|
||||||
@@ -32,13 +31,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public object? Underlying => throw new NotImplementedException();
|
public object? Underlying => throw new NotImplementedException();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public SystemTextJsonMessageAccessor()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -48,9 +40,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
#endif
|
||||||
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
|
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
|
||||||
{
|
{
|
||||||
if (!IsJson)
|
if (!IsValid)
|
||||||
return new CallResult<object>(GetOriginalString());
|
return new CallResult<object>(GetOriginalString());
|
||||||
|
|
||||||
if (_document == null)
|
if (_document == null)
|
||||||
@@ -58,22 +54,25 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = _document.Deserialize(type, _customSerializerOptions ?? _serializerOptions);
|
var result = _document.Deserialize(type, _customSerializerOptions);
|
||||||
return new CallResult<object>(result!);
|
return new CallResult<object>(result!);
|
||||||
}
|
}
|
||||||
catch (JsonException ex)
|
catch (JsonException ex)
|
||||||
{
|
{
|
||||||
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
||||||
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
return new CallResult<object>(new DeserializeError(info, ex));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
var info = $"Deserialize unknown Exception: {ex.Message}";
|
return new CallResult<object>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
||||||
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
#endif
|
||||||
public CallResult<T> Deserialize<T>(MessagePath? path = null)
|
public CallResult<T> Deserialize<T>(MessagePath? path = null)
|
||||||
{
|
{
|
||||||
if (_document == null)
|
if (_document == null)
|
||||||
@@ -81,25 +80,24 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = _document.Deserialize<T>(_customSerializerOptions ?? _serializerOptions);
|
var result = _document.Deserialize<T>(_customSerializerOptions);
|
||||||
return new CallResult<T>(result!);
|
return new CallResult<T>(result!);
|
||||||
}
|
}
|
||||||
catch (JsonException ex)
|
catch (JsonException ex)
|
||||||
{
|
{
|
||||||
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
||||||
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
return new CallResult<T>(new DeserializeError(info, ex));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
var info = $"Unknown exception: {ex.Message}";
|
return new CallResult<T>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
||||||
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public NodeType? GetNodeType()
|
public NodeType? GetNodeType()
|
||||||
{
|
{
|
||||||
if (!IsJson)
|
if (!IsValid)
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||||
|
|
||||||
if (_document == null)
|
if (_document == null)
|
||||||
@@ -116,7 +114,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public NodeType? GetNodeType(MessagePath path)
|
public NodeType? GetNodeType(MessagePath path)
|
||||||
{
|
{
|
||||||
if (!IsJson)
|
if (!IsValid)
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||||
|
|
||||||
var node = GetPathNode(path);
|
var node = GetPathNode(path);
|
||||||
@@ -132,9 +130,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
#endif
|
||||||
public T? GetValue<T>(MessagePath path)
|
public T? GetValue<T>(MessagePath path)
|
||||||
{
|
{
|
||||||
if (!IsJson)
|
if (!IsValid)
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||||
|
|
||||||
var value = GetPathNode(path);
|
var value = GetPathNode(path);
|
||||||
@@ -145,7 +147,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return value.Value.Deserialize<T>(_customSerializerOptions ?? _serializerOptions);
|
return value.Value.Deserialize<T>(_customSerializerOptions);
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
|
|
||||||
@@ -158,13 +160,17 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
return (T)(object)value.Value.GetInt64().ToString();
|
return (T)(object)value.Value.GetInt64().ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return value.Value.Deserialize<T>();
|
return value.Value.Deserialize<T>(_customSerializerOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public List<T?>? GetValues<T>(MessagePath path)
|
#if NET5_0_OR_GREATER
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
#endif
|
||||||
|
public T?[]? GetValues<T>(MessagePath path)
|
||||||
{
|
{
|
||||||
if (!IsJson)
|
if (!IsValid)
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||||
|
|
||||||
var value = GetPathNode(path);
|
var value = GetPathNode(path);
|
||||||
@@ -174,12 +180,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
if (value.Value.ValueKind != JsonValueKind.Array)
|
if (value.Value.ValueKind != JsonValueKind.Array)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
return value.Value.Deserialize<List<T>>()!;
|
return value.Value.Deserialize<T[]>(_customSerializerOptions)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
private JsonElement? GetPathNode(MessagePath path)
|
private JsonElement? GetPathNode(MessagePath path)
|
||||||
{
|
{
|
||||||
if (!IsJson)
|
if (!IsValid)
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||||
|
|
||||||
if (_document == null)
|
if (_document == null)
|
||||||
@@ -240,13 +246,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public SystemTextJsonStreamMessageAccessor(): base()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -277,14 +276,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
|
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
|
||||||
IsJson = true;
|
IsValid = true;
|
||||||
return new CallResult(null);
|
return CallResult.SuccessResult;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Not a json message
|
// Not a json message
|
||||||
IsJson = false;
|
IsValid = false;
|
||||||
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,13 +316,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
{
|
{
|
||||||
private ReadOnlyMemory<byte> _bytes;
|
private ReadOnlyMemory<byte> _bytes;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public SystemTextJsonByteMessageAccessor() : base()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -342,19 +334,19 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
if (firstByte != 0x7b && firstByte != 0x5b)
|
if (firstByte != 0x7b && firstByte != 0x5b)
|
||||||
{
|
{
|
||||||
// Value doesn't start with `{` or `[`, prevent deserialization attempt as it's slow
|
// Value doesn't start with `{` or `[`, prevent deserialization attempt as it's slow
|
||||||
IsJson = false;
|
IsValid = false;
|
||||||
return new CallResult(new ServerError("Not a json value"));
|
return new CallResult(new DeserializeError("Not a json value"));
|
||||||
}
|
}
|
||||||
|
|
||||||
_document = JsonDocument.Parse(data);
|
_document = JsonDocument.Parse(data);
|
||||||
IsJson = true;
|
IsValid = true;
|
||||||
return new CallResult(null);
|
return CallResult.SuccessResult;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Not a json message
|
// Not a json message
|
||||||
IsJson = false;
|
IsValid = false;
|
||||||
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,27 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public class SystemTextJsonMessageSerializer : IMessageSerializer
|
public class SystemTextJsonMessageSerializer : IStringMessageSerializer
|
||||||
{
|
{
|
||||||
|
private readonly JsonSerializerOptions _options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public SystemTextJsonMessageSerializer(JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Serialize(object message) => JsonSerializer.Serialize(message, SerializerOptions.WithConverters);
|
#if NET5_0_OR_GREATER
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
|
||||||
|
#endif
|
||||||
|
public string Serialize<T>(T message) => JsonSerializer.Serialize(message, _options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net9.0</TargetFrameworks>
|
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<PackageId>CryptoExchange.Net</PackageId>
|
<PackageId>CryptoExchange.Net</PackageId>
|
||||||
<Authors>JKorf</Authors>
|
<Authors>JKorf</Authors>
|
||||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||||
<PackageVersion>8.8.0</PackageVersion>
|
<PackageVersion>10.1.0</PackageVersion>
|
||||||
<AssemblyVersion>8.8.0</AssemblyVersion>
|
<AssemblyVersion>10.1.0</AssemblyVersion>
|
||||||
<FileVersion>8.8.0</FileVersion>
|
<FileVersion>10.1.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>
|
||||||
@@ -20,13 +20,16 @@
|
|||||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
|
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<LangVersion>12.0</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="Icon\icon.png" Pack="true" PackagePath="\" />
|
<None Include="Icon\icon.png" Pack="true" PackagePath="\" />
|
||||||
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<PropertyGroup Label="AOT" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
|
||||||
|
<IsAotCompatible>true</IsAotCompatible>
|
||||||
|
</PropertyGroup>
|
||||||
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
<IncludeSymbols>true</IncludeSymbols>
|
<IncludeSymbols>true</IncludeSymbols>
|
||||||
@@ -34,12 +37,6 @@
|
|||||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
|
||||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
</ItemGroup>
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<DocumentationFile>CryptoExchange.Net.xml</DocumentationFile>
|
<DocumentationFile>CryptoExchange.Net.xml</DocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -48,17 +45,19 @@
|
|||||||
<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.CodeAnalysis.NetAnalyzers" Version="9.0.0">
|
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.101">
|
||||||
<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="10.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
<PackageReference Include="System.Text.Json" Version="10.0.1" />
|
||||||
<PackageReference Include="System.Text.Json" Version="9.0.0" />
|
<PackageReference Include="NSec.Cryptography" Version="25.4.0" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
|
</ItemGroup>
|
||||||
|
<ItemGroup Label="Transitive Client Packages">
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
|
||||||
|
<PackageReference Include="System.Threading.Channels" Version="10.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Exceptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Exception during deserialization
|
||||||
|
/// </summary>
|
||||||
|
public class CeDeserializationException : Exception
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public CeDeserializationException(string message) : base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public CeDeserializationException(string message, Exception innerException) : base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -15,6 +17,7 @@ namespace CryptoExchange.Net
|
|||||||
public static class ExchangeHelpers
|
public static class ExchangeHelpers
|
||||||
{
|
{
|
||||||
private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789";
|
private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789";
|
||||||
|
private const string _allowedRandomHexChars = "0123456789ABCDEF";
|
||||||
|
|
||||||
private static readonly Dictionary<int, string> _monthSymbols = new Dictionary<int, string>()
|
private static readonly Dictionary<int, string> _monthSymbols = new Dictionary<int, string>()
|
||||||
{
|
{
|
||||||
@@ -86,8 +89,6 @@ namespace CryptoExchange.Net
|
|||||||
value -= offset;
|
value -= offset;
|
||||||
else value += (step.Value - offset);
|
else value += (step.Value - offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
value = RoundDown(value, 8);
|
|
||||||
|
|
||||||
return value.Normalize();
|
return value.Normalize();
|
||||||
}
|
}
|
||||||
@@ -111,6 +112,34 @@ namespace CryptoExchange.Net
|
|||||||
return RoundToSignificantDigits(value, precision.Value, roundingType);
|
return RoundToSignificantDigits(value, precision.Value, roundingType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Apply the provided rules to the value
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">Value to be adjusted</param>
|
||||||
|
/// <param name="decimals">Max decimal places</param>
|
||||||
|
/// <param name="valueStep">The value step for increase/decrease value</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static decimal ApplyRules(
|
||||||
|
decimal value,
|
||||||
|
int? decimals = null,
|
||||||
|
decimal? valueStep = null)
|
||||||
|
{
|
||||||
|
if (valueStep.HasValue)
|
||||||
|
{
|
||||||
|
var offset = value % valueStep.Value;
|
||||||
|
if (offset != 0)
|
||||||
|
{
|
||||||
|
if (offset < valueStep.Value / 2)
|
||||||
|
value -= offset;
|
||||||
|
else value += (valueStep.Value - offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (decimals.HasValue)
|
||||||
|
value = Math.Round(value, decimals.Value);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12
|
/// Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -192,6 +221,62 @@ namespace CryptoExchange.Net
|
|||||||
return new string(randomChars);
|
return new string(randomChars);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate a random string of specified length
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="length">Length of the random string</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string RandomHexString(int length)
|
||||||
|
{
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
return "0x" + RandomNumberGenerator.GetHexString(length * 2);
|
||||||
|
#else
|
||||||
|
var randomChars = new char[length * 2];
|
||||||
|
var random = new Random();
|
||||||
|
for (int i = 0; i < length * 2; i++)
|
||||||
|
randomChars[i] = _allowedRandomHexChars[random.Next(0, _allowedRandomHexChars.Length)];
|
||||||
|
return "0x" + new string(randomChars);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate a long value
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="maxLength">Max number of digits</param>
|
||||||
|
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 long value between two values
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="minValue">Min value</param>
|
||||||
|
/// <param name="maxValue">Max value</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static long RandomLong(long minValue, long maxValue)
|
||||||
|
{
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
var buf = RandomNumberGenerator.GetBytes(8);
|
||||||
|
#else
|
||||||
|
byte[] buf = new byte[8];
|
||||||
|
var random = new Random();
|
||||||
|
random.NextBytes(buf);
|
||||||
|
#endif
|
||||||
|
long longRand = BitConverter.ToInt64(buf, 0);
|
||||||
|
return (Math.Abs(longRand % (maxValue - minValue)) + minValue);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generate a random string of specified length
|
/// Generate a random string of specified length
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -225,10 +310,10 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="request">The request parameters</param>
|
/// <param name="request">The request parameters</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static async IAsyncEnumerable<ExchangeWebResult<IEnumerable<T>>> ExecutePages<T, U>(Func<U, INextPageToken?, CancellationToken, Task<ExchangeWebResult<IEnumerable<T>>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
|
public static async IAsyncEnumerable<ExchangeWebResult<T[]>> ExecutePages<T, U>(Func<U, INextPageToken?, CancellationToken, Task<ExchangeWebResult<T[]>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var result = new List<T>();
|
var result = new List<T>();
|
||||||
ExchangeWebResult<IEnumerable<T>> batch;
|
ExchangeWebResult<T[]> batch;
|
||||||
INextPageToken? nextPageToken = null;
|
INextPageToken? nextPageToken = null;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
@@ -274,5 +359,146 @@ namespace CryptoExchange.Net
|
|||||||
adjustedQuantity = symbol.QuantityDecimals.HasValue ? (minNotionalAdjust ? RoundUp(adjustedQuantity, symbol.QuantityDecimals.Value) : RoundDown(adjustedQuantity, symbol.QuantityDecimals.Value)) : adjustedQuantity;
|
adjustedQuantity = symbol.QuantityDecimals.HasValue ? (minNotionalAdjust ? RoundUp(adjustedQuantity, symbol.QuantityDecimals.Value) : RoundDown(adjustedQuantity, symbol.QuantityDecimals.Value)) : adjustedQuantity;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Queue updates received from a websocket subscriptions and process them async
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The queued update type</typeparam>
|
||||||
|
/// <param name="subscribeCall">The subscribe call</param>
|
||||||
|
/// <param name="asyncHandler">The async update handler</param>
|
||||||
|
/// <param name="maxQueuedItems">The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with <see>fullMode</see></param>
|
||||||
|
/// <param name="fullBehavior">What should happen if the queue contains <see>maxQueuedItems</see> pending updates. If no max is set this setting is ignored</param>
|
||||||
|
public static async Task<CallResult<UpdateSubscription>> ProcessQueuedAsync<T>(
|
||||||
|
Func<Action<DataEvent<T>>, Task<CallResult<UpdateSubscription>>> subscribeCall,
|
||||||
|
Func<DataEvent<T>, Task> asyncHandler,
|
||||||
|
int? maxQueuedItems = null,
|
||||||
|
QueueFullBehavior? fullBehavior = null)
|
||||||
|
{
|
||||||
|
var processor = new ProcessQueue<DataEvent<T>>(asyncHandler, maxQueuedItems, fullBehavior);
|
||||||
|
await processor.StartAsync().ConfigureAwait(false);
|
||||||
|
var result = await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false);
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
await processor.StopAsync().ConfigureAwait(false);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
processor.Exception += result.Data._subscription.InvokeExceptionHandler;
|
||||||
|
result.Data.SubscriptionStatusChanged += (upd) =>
|
||||||
|
{
|
||||||
|
if (upd == CryptoExchange.Net.Objects.SubscriptionStatus.Closed)
|
||||||
|
_ = processor.StopAsync(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Queue updates and process them async
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The queued update type</typeparam>
|
||||||
|
/// <param name="subscribeCall">The subscribe call</param>
|
||||||
|
/// <param name="asyncHandler">The async update handler</param>
|
||||||
|
/// <param name="maxQueuedItems">The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with <see>fullMode</see></param>
|
||||||
|
/// <param name="fullBehavior">What should happen if the queue contains <see>maxQueuedItems</see> pending updates. If no max is set this setting is ignored</param>
|
||||||
|
/// <param name="ct">Cancellation token to stop the processing</param>
|
||||||
|
public static async Task ProcessQueuedAsync<T>(
|
||||||
|
Func<Action<T>, Task> subscribeCall,
|
||||||
|
Func<T, Task> asyncHandler,
|
||||||
|
CancellationToken ct,
|
||||||
|
int? maxQueuedItems = null,
|
||||||
|
QueueFullBehavior? fullBehavior = null)
|
||||||
|
{
|
||||||
|
var processor = new ProcessQueue<T>(asyncHandler, maxQueuedItems, fullBehavior);
|
||||||
|
await processor.StartAsync().ConfigureAwait(false);
|
||||||
|
ct.Register(async () =>
|
||||||
|
{
|
||||||
|
await processor.StopAsync().ConfigureAwait(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Queue updates received from a websocket subscriptions and process them async
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TEventType">The type of the queued item</typeparam>
|
||||||
|
/// <typeparam name="TOutputType">The type of the item to pass to the processor</typeparam>
|
||||||
|
/// <param name="subscribeCall">The subscribe call</param>
|
||||||
|
/// <param name="mapper">The mapper function to go from <see>TEventType</see> to <see>TOutputType</see></param>
|
||||||
|
/// <param name="asyncHandler">The async update handler</param>
|
||||||
|
/// <param name="maxQueuedItems">The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with <see>fullMode</see></param>
|
||||||
|
/// <param name="fullBehavior">What should happen if the queue contains <see>maxQueuedItems</see> pending updates. If no max is set this setting is ignored</param>
|
||||||
|
public static async Task<CallResult<UpdateSubscription>> ProcessQueuedAsync<TEventType, TOutputType>(
|
||||||
|
Func<ProcessQueue<DataEvent<TEventType>>, Task<CallResult<UpdateSubscription>>> subscribeCall,
|
||||||
|
Func<DataEvent<TEventType>, DataEvent<TOutputType>> mapper,
|
||||||
|
Func<DataEvent<TOutputType>, Task> asyncHandler,
|
||||||
|
int? maxQueuedItems = null,
|
||||||
|
QueueFullBehavior? fullBehavior = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var processor = new ProcessQueue<DataEvent<TEventType>>((update) => {
|
||||||
|
return asyncHandler.Invoke(mapper.Invoke(update));
|
||||||
|
}, maxQueuedItems, fullBehavior);
|
||||||
|
await processor.StartAsync().ConfigureAwait(false);
|
||||||
|
var result = await subscribeCall(processor).ConfigureAwait(false);
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
await processor.StopAsync().ConfigureAwait(false);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
processor.Exception += result.Data._subscription.InvokeExceptionHandler;
|
||||||
|
result.Data.SubscriptionStatusChanged += (upd) =>
|
||||||
|
{
|
||||||
|
if (upd == SubscriptionStatus.Closed)
|
||||||
|
_ = processor.StopAsync(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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,69 @@
|
|||||||
|
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,15 +1,15 @@
|
|||||||
using System;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO.Compression;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Web;
|
using System.Web;
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using System.Globalization;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using CryptoExchange.Net.SharedApis;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
@@ -61,30 +61,71 @@ namespace CryptoExchange.Net
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static string CreateParamString(this IDictionary<string, object> parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType)
|
public static string CreateParamString(this IDictionary<string, object> parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType)
|
||||||
{
|
{
|
||||||
var uriString = string.Empty;
|
var uriString = new StringBuilder();
|
||||||
var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList();
|
bool first = true;
|
||||||
foreach (var arrayEntry in arraysParameters)
|
foreach(var parameter in parameters)
|
||||||
{
|
{
|
||||||
if (serializationType == ArrayParametersSerialization.Array)
|
if (!first)
|
||||||
|
uriString.Append("&");
|
||||||
|
|
||||||
|
first = false;
|
||||||
|
|
||||||
|
if (parameter.Value.GetType().IsArray)
|
||||||
{
|
{
|
||||||
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()!) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={string.Format(CultureInfo.InvariantCulture, "{0}", v)}"))}&";
|
if (serializationType == ArrayParametersSerialization.Array)
|
||||||
|
{
|
||||||
|
foreach(var entry in (object[])parameter.Value)
|
||||||
|
{
|
||||||
|
uriString.Append(parameter.Key);
|
||||||
|
uriString.Append("[]=");
|
||||||
|
if (urlEncodeValues)
|
||||||
|
uriString.Append(Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", entry)));
|
||||||
|
else
|
||||||
|
uriString.Append(string.Format(CultureInfo.InvariantCulture, "{0}", entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (serializationType == ArrayParametersSerialization.MultipleValues)
|
||||||
|
{
|
||||||
|
foreach (var entry in (object[])parameter.Value)
|
||||||
|
{
|
||||||
|
uriString.Append(parameter.Key);
|
||||||
|
uriString.Append("=");
|
||||||
|
if (urlEncodeValues)
|
||||||
|
uriString.Append(Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", entry)));
|
||||||
|
else
|
||||||
|
uriString.Append(string.Format(CultureInfo.InvariantCulture, "{0}", entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
uriString.Append('[');
|
||||||
|
var firstArrayEntry = true;
|
||||||
|
foreach (var entry in (object[])parameter.Value)
|
||||||
|
{
|
||||||
|
if (!firstArrayEntry)
|
||||||
|
uriString.Append(',');
|
||||||
|
|
||||||
|
firstArrayEntry = false;
|
||||||
|
if (urlEncodeValues)
|
||||||
|
uriString.Append(Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", entry)));
|
||||||
|
else
|
||||||
|
uriString.Append(string.Format(CultureInfo.InvariantCulture, "{0}", entry));
|
||||||
|
}
|
||||||
|
uriString.Append(']');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (serializationType == ArrayParametersSerialization.MultipleValues)
|
else
|
||||||
{
|
{
|
||||||
var array = (Array)arrayEntry.Value;
|
uriString.Append(parameter.Key);
|
||||||
uriString += string.Join("&", array.OfType<object>().Select(a => $"{arrayEntry.Key}={Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", a))}"));
|
uriString.Append('=');
|
||||||
uriString += "&";
|
if (urlEncodeValues)
|
||||||
}
|
uriString.Append(Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", parameter.Value)));
|
||||||
else
|
else
|
||||||
{
|
uriString.Append(string.Format(CultureInfo.InvariantCulture, "{0}", parameter.Value));
|
||||||
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))}"))}";
|
return uriString.ToString();
|
||||||
uriString = uriString.TrimEnd('&');
|
|
||||||
return uriString;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -230,18 +271,16 @@ namespace CryptoExchange.Net
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Append a base url with provided path
|
/// Append a base url with provided path
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="url"></param>
|
|
||||||
/// <param name="path"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string AppendPath(this string url, params string[] path)
|
public static string AppendPath(this string url, params string[] path)
|
||||||
{
|
{
|
||||||
if (!url.EndsWith("/"))
|
var sb = new StringBuilder(url.TrimEnd('/'));
|
||||||
url += "/";
|
foreach (var subPath in path)
|
||||||
|
{
|
||||||
|
sb.Append('/');
|
||||||
|
sb.Append(subPath.Trim('/'));
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var item in path)
|
return sb.ToString();
|
||||||
url += item.Trim('/') + "/";
|
|
||||||
|
|
||||||
return url.TrimEnd('/');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -363,19 +402,40 @@ namespace CryptoExchange.Net
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Decompress using GzipStream
|
/// Decompress using GzipStream
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="data"></param>
|
public static ReadOnlySpan<byte> DecompressGzip(this ReadOnlySpan<byte> data)
|
||||||
/// <returns></returns>
|
{
|
||||||
|
using var decompressedStream = new MemoryStream();
|
||||||
|
using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress);
|
||||||
|
deflateStream.CopyTo(decompressedStream);
|
||||||
|
return new ReadOnlySpan<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decompress using GzipStream
|
||||||
|
/// </summary>
|
||||||
public static ReadOnlyMemory<byte> DecompressGzip(this ReadOnlyMemory<byte> data)
|
public static ReadOnlyMemory<byte> DecompressGzip(this ReadOnlyMemory<byte> data)
|
||||||
{
|
{
|
||||||
using var decompressedStream = new MemoryStream();
|
using var decompressedStream = new MemoryStream();
|
||||||
using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment)
|
using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment)
|
||||||
? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count)
|
? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count)
|
||||||
: new MemoryStream(data.ToArray());
|
: new MemoryStream(data.ToArray());
|
||||||
using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress);
|
using var deflateStream = new GZipStream(dataStream, CompressionMode.Decompress);
|
||||||
deflateStream.CopyTo(decompressedStream);
|
deflateStream.CopyTo(decompressedStream);
|
||||||
return new ReadOnlyMemory<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length);
|
return new ReadOnlyMemory<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decompress using GzipStream
|
||||||
|
/// </summary>
|
||||||
|
public static ReadOnlySpan<byte> Decompress(this ReadOnlySpan<byte> input)
|
||||||
|
{
|
||||||
|
using var output = new MemoryStream();
|
||||||
|
using var compressStream = new MemoryStream(input.ToArray());
|
||||||
|
using var decompressor = new DeflateStream(compressStream, CompressionMode.Decompress);
|
||||||
|
decompressor.CopyTo(output);
|
||||||
|
return new ReadOnlySpan<byte>(output.GetBuffer(), 0, (int)output.Length);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Decompress using DeflateStream
|
/// Decompress using DeflateStream
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -385,10 +445,9 @@ namespace CryptoExchange.Net
|
|||||||
{
|
{
|
||||||
var output = new MemoryStream();
|
var output = new MemoryStream();
|
||||||
|
|
||||||
using (var compressStream = new MemoryStream(input.ToArray()))
|
using var compressStream = new MemoryStream(input.ToArray());
|
||||||
using (var decompressor = new DeflateStream(compressStream, CompressionMode.Decompress))
|
using var decompressor = new DeflateStream(compressStream, CompressionMode.Decompress);
|
||||||
decompressor.CopyTo(output);
|
decompressor.CopyTo(output);
|
||||||
|
|
||||||
output.Position = 0;
|
output.Position = 0;
|
||||||
return new ReadOnlyMemory<byte>(output.GetBuffer(), 0, (int)output.Length);
|
return new ReadOnlyMemory<byte>(output.GetBuffer(), 0, (int)output.Length);
|
||||||
}
|
}
|
||||||
@@ -396,7 +455,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the trading mode is linear
|
/// Whether the trading mode is linear
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool IsLinear(this TradingMode type) => type == TradingMode.PerpetualLinear || type == TradingMode.DeliveryLinear;
|
public static bool IsLinear(this TradingMode type) => type == TradingMode.PerpetualLinear || type == TradingMode.DeliveryLinear;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the trading mode is inverse
|
/// Whether the trading mode is inverse
|
||||||
@@ -413,6 +472,36 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool IsDelivery(this TradingMode type) => type == TradingMode.DeliveryInverse || type == TradingMode.DeliveryLinear;
|
public static bool IsDelivery(this TradingMode type) => type == TradingMode.DeliveryInverse || type == TradingMode.DeliveryLinear;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the account type is a futures account
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsFuturesAccount(this SharedAccountType type) =>
|
||||||
|
type == SharedAccountType.PerpetualLinearFutures
|
||||||
|
|| type == SharedAccountType.DeliveryLinearFutures
|
||||||
|
|| type == SharedAccountType.PerpetualInverseFutures
|
||||||
|
|| type == SharedAccountType.DeliveryInverseFutures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the account type is a margin account
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsMarginAccount(this SharedAccountType type) =>
|
||||||
|
type == SharedAccountType.CrossMargin
|
||||||
|
|| type == SharedAccountType.IsolatedMargin;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Map a TradingMode value to a SharedAccountType enum value
|
||||||
|
/// </summary>
|
||||||
|
public static SharedAccountType ToAccountType(this TradingMode mode)
|
||||||
|
{
|
||||||
|
if (mode == TradingMode.Spot) return SharedAccountType.Spot;
|
||||||
|
if (mode == TradingMode.PerpetualLinear) return SharedAccountType.PerpetualLinearFutures;
|
||||||
|
if (mode == TradingMode.PerpetualInverse) return SharedAccountType.PerpetualInverseFutures;
|
||||||
|
if (mode == TradingMode.DeliveryInverse) return SharedAccountType.DeliveryInverseFutures;
|
||||||
|
if (mode == TradingMode.DeliveryLinear) return SharedAccountType.DeliveryLinearFutures;
|
||||||
|
|
||||||
|
throw new ArgumentException(nameof(mode), "Unmapped trading mode");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Register rest client interfaces
|
/// Register rest client interfaces
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -440,6 +529,10 @@ namespace CryptoExchange.Net
|
|||||||
services.AddTransient(x => (IWithdrawRestClient)client(x)!);
|
services.AddTransient(x => (IWithdrawRestClient)client(x)!);
|
||||||
if (typeof(IFeeRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(IFeeRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (IFeeRestClient)client(x)!);
|
services.AddTransient(x => (IFeeRestClient)client(x)!);
|
||||||
|
if (typeof(IBookTickerRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IBookTickerRestClient)client(x)!);
|
||||||
|
if (typeof(ITransferRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (ITransferRestClient)client(x)!);
|
||||||
|
|
||||||
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
|
services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
|
||||||
@@ -447,6 +540,10 @@ namespace CryptoExchange.Net
|
|||||||
services.AddTransient(x => (ISpotSymbolRestClient)client(x)!);
|
services.AddTransient(x => (ISpotSymbolRestClient)client(x)!);
|
||||||
if (typeof(ISpotTickerRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(ISpotTickerRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (ISpotTickerRestClient)client(x)!);
|
services.AddTransient(x => (ISpotTickerRestClient)client(x)!);
|
||||||
|
if (typeof(ISpotTriggerOrderRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (ISpotTriggerOrderRestClient)client(x)!);
|
||||||
|
if (typeof(ISpotOrderClientIdRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (ISpotOrderClientIdRestClient)client(x)!);
|
||||||
|
|
||||||
if (typeof(IFundingRateRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(IFundingRateRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (IFundingRateRestClient)client(x)!);
|
services.AddTransient(x => (IFundingRateRestClient)client(x)!);
|
||||||
@@ -468,6 +565,12 @@ namespace CryptoExchange.Net
|
|||||||
services.AddTransient(x => (IPositionHistoryRestClient)client(x)!);
|
services.AddTransient(x => (IPositionHistoryRestClient)client(x)!);
|
||||||
if (typeof(IPositionModeRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(IPositionModeRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (IPositionModeRestClient)client(x)!);
|
services.AddTransient(x => (IPositionModeRestClient)client(x)!);
|
||||||
|
if (typeof(IFuturesTpSlRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IFuturesTpSlRestClient)client(x)!);
|
||||||
|
if (typeof(IFuturesTriggerOrderRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IFuturesTriggerOrderRestClient)client(x)!);
|
||||||
|
if (typeof(IFuturesOrderClientIdRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IFuturesOrderClientIdRestClient)client(x)!);
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
@@ -483,8 +586,8 @@ namespace CryptoExchange.Net
|
|||||||
services.AddTransient(x => (IBookTickerSocketClient)client(x)!);
|
services.AddTransient(x => (IBookTickerSocketClient)client(x)!);
|
||||||
if (typeof(IKlineSocketClient).IsAssignableFrom(typeof(T)))
|
if (typeof(IKlineSocketClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (IKlineSocketClient)client(x)!);
|
services.AddTransient(x => (IKlineSocketClient)client(x)!);
|
||||||
if (typeof(IOrderBookRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(IOrderBookSocketClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (IOrderBookRestClient)client(x)!);
|
services.AddTransient(x => (IOrderBookSocketClient)client(x)!);
|
||||||
if (typeof(ITickerSocketClient).IsAssignableFrom(typeof(T)))
|
if (typeof(ITickerSocketClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (ITickerSocketClient)client(x)!);
|
services.AddTransient(x => (ITickerSocketClient)client(x)!);
|
||||||
if (typeof(ITickersSocketClient).IsAssignableFrom(typeof(T)))
|
if (typeof(ITickersSocketClient).IsAssignableFrom(typeof(T)))
|
||||||
|
|||||||
+1
-2
@@ -1,10 +1,9 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces
|
namespace CryptoExchange.Net.Interfaces.Clients
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base api client
|
/// Base api client
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Interfaces.Clients
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Client for accessing REST API's for different exchanges
|
||||||
|
/// </summary>
|
||||||
|
public interface ICryptoRestClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Try get
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <returns></returns>
|
||||||
|
T TryGet<T>(Func<T> createFunc);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user