mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2025-07-17 06:55:41 +00:00
parent
0e7d49991a
commit
96f23f163d
@ -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(ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
/// <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(ex.ToLogString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// <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("ProtoBufError: " + 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(ex.ToLogString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// <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(ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
/// <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("ProtobufError: " + 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,53 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<PackageId>CryptoExchange.Net.Protobuf</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>Protobuf support for CryptoExchange.Net</Description>
|
||||
<PackageVersion>9.1.0</PackageVersion>
|
||||
<AssemblyVersion>9.1.0</AssemblyVersion>
|
||||
<FileVersion>9.1.0</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net/tree/master/CryptoExchange.Net.Protobuf</PackageProjectUrl>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\CryptoExchange.Net\Icon\icon.png" Pack="true" PackagePath="\" />
|
||||
<None Include="README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="AOT" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="protobuf-net" Version="3.2.52" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CryptoExchange.Net\CryptoExchange.Net.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
129
CryptoExchange.Net.Protobuf/CryptoExchange.Net.Protobuf.xml
Normal file
129
CryptoExchange.Net.Protobuf/CryptoExchange.Net.Protobuf.xml
Normal file
@ -0,0 +1,129 @@
|
||||
<?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>
|
7
CryptoExchange.Net.Protobuf/README.md
Normal file
7
CryptoExchange.Net.Protobuf/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
#  CryptoExchange.Net.Proto
|
||||
|
||||
[](https://github.com/JKorf/CryptoExchange.NetProtobuf/actions/workflows/dotnet.yml) [](https://www.nuget.org/packages/CryptoExchange.NetProtobuf) 
|
||||
|
||||
Protobuf support for CryptoExchange.Net.
|
||||
|
||||
## Release notes
|
@ -17,6 +17,7 @@ using CryptoExchange.Net.Testing.Implementations;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Options;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using System.Net.WebSockets;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
@ -98,7 +99,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
|
||||
}
|
||||
|
||||
protected internal override IByteMessageAccessor CreateAccessor() => new SystemTextJsonByteMessageAccessor(new System.Text.Json.JsonSerializerOptions());
|
||||
protected internal override IByteMessageAccessor CreateAccessor(WebSocketMessageType type) => new SystemTextJsonByteMessageAccessor(new System.Text.Json.JsonSerializerOptions());
|
||||
protected internal override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -119,7 +120,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
|
||||
public override string GetListenerIdentifier(IMessageAccessor message)
|
||||
{
|
||||
if (!message.IsJson)
|
||||
if (!message.IsValid)
|
||||
{
|
||||
return "topic";
|
||||
}
|
||||
|
@ -465,10 +465,13 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <returns></returns>
|
||||
protected static string GetSerializedBody(IMessageSerializer serializer, IDictionary<string, object> parameters)
|
||||
{
|
||||
if (serializer is not IStringMessageSerializer stringSerializer)
|
||||
throw new InvalidOperationException("Non-string message serializer can't get serialized request body");
|
||||
|
||||
if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
|
||||
return serializer.Serialize(value);
|
||||
return stringSerializer.Serialize(value);
|
||||
else
|
||||
return serializer.Serialize(parameters);
|
||||
return stringSerializer.Serialize(parameters);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -603,12 +603,16 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
if (contentType == Constants.JsonContentHeader)
|
||||
{
|
||||
var serializer = CreateSerializer();
|
||||
if (serializer is not IStringMessageSerializer stringSerializer)
|
||||
throw new InvalidOperationException("Non-string message serializer can't get serialized request body");
|
||||
|
||||
// Write the parameters as json in the body
|
||||
string stringData;
|
||||
if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
|
||||
stringData = CreateSerializer().Serialize(value);
|
||||
stringData = stringSerializer.Serialize(value);
|
||||
else
|
||||
stringData = CreateSerializer().Serialize(parameters);
|
||||
stringData = stringSerializer.Serialize(parameters);
|
||||
request.SetContent(stringData, contentType);
|
||||
}
|
||||
else if (contentType == Constants.FormContentHeader)
|
||||
|
@ -138,7 +138,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// Create a message accessor instance
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected internal abstract IByteMessageAccessor CreateAccessor();
|
||||
protected internal abstract IByteMessageAccessor CreateAccessor(WebSocketMessageType messageType);
|
||||
|
||||
/// <summary>
|
||||
/// Create a serializer instance
|
||||
|
@ -19,22 +19,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
var value = reader.GetString();
|
||||
if (string.IsNullOrEmpty(value) || string.Equals("null", value, StringComparison.OrdinalIgnoreCase))
|
||||
return null;
|
||||
|
||||
if (string.Equals("Infinity", value, StringComparison.Ordinal))
|
||||
// Infinity returned by the server, default to max value
|
||||
return decimal.MaxValue;
|
||||
|
||||
try
|
||||
{
|
||||
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch(OverflowException)
|
||||
{
|
||||
// Value doesn't fit decimal, default to max value
|
||||
return decimal.MaxValue;
|
||||
}
|
||||
return ExchangeHelpers.ParseDecimal(value);
|
||||
}
|
||||
|
||||
try
|
||||
|
@ -24,7 +24,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
private readonly JsonSerializerOptions? _customSerializerOptions;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsJson { get; set; }
|
||||
public bool IsValid { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract bool OriginalDataAvailable { get; }
|
||||
@ -47,7 +47,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
#endif
|
||||
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
|
||||
{
|
||||
if (!IsJson)
|
||||
if (!IsValid)
|
||||
return new CallResult<object>(GetOriginalString());
|
||||
|
||||
if (_document == null)
|
||||
@ -100,7 +100,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
/// <inheritdoc />
|
||||
public NodeType? GetNodeType()
|
||||
{
|
||||
if (!IsJson)
|
||||
if (!IsValid)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
if (_document == null)
|
||||
@ -117,7 +117,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
/// <inheritdoc />
|
||||
public NodeType? GetNodeType(MessagePath path)
|
||||
{
|
||||
if (!IsJson)
|
||||
if (!IsValid)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
var node = GetPathNode(path);
|
||||
@ -139,7 +139,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
#endif
|
||||
public T? GetValue<T>(MessagePath path)
|
||||
{
|
||||
if (!IsJson)
|
||||
if (!IsValid)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
var value = GetPathNode(path);
|
||||
@ -173,7 +173,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
#endif
|
||||
public T?[]? GetValues<T>(MessagePath path)
|
||||
{
|
||||
if (!IsJson)
|
||||
if (!IsValid)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
var value = GetPathNode(path);
|
||||
@ -188,7 +188,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
|
||||
private JsonElement? GetPathNode(MessagePath path)
|
||||
{
|
||||
if (!IsJson)
|
||||
if (!IsValid)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
if (_document == null)
|
||||
@ -279,13 +279,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
try
|
||||
{
|
||||
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
|
||||
IsJson = true;
|
||||
IsValid = true;
|
||||
return CallResult.SuccessResult;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Not a json message
|
||||
IsJson = false;
|
||||
IsValid = false;
|
||||
return new CallResult(new DeserializeError("JsonError: " + ex.Message, ex));
|
||||
}
|
||||
}
|
||||
@ -337,18 +337,18 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
if (firstByte != 0x7b && firstByte != 0x5b)
|
||||
{
|
||||
// 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"));
|
||||
}
|
||||
|
||||
_document = JsonDocument.Parse(data);
|
||||
IsJson = true;
|
||||
IsValid = true;
|
||||
return CallResult.SuccessResult;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Not a json message
|
||||
IsJson = false;
|
||||
IsValid = false;
|
||||
return new CallResult(new DeserializeError("JsonError: " + ex.Message, ex));
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ using System.Text.Json.Serialization.Metadata;
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class SystemTextJsonMessageSerializer : IMessageSerializer
|
||||
public class SystemTextJsonMessageSerializer : IStringMessageSerializer
|
||||
{
|
||||
private readonly JsonSerializerOptions _options;
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
@ -341,5 +342,28 @@ namespace CryptoExchange.Net
|
||||
adjustedQuantity = symbol.QuantityDecimals.HasValue ? (minNotionalAdjust ? RoundUp(adjustedQuantity, symbol.QuantityDecimals.Value) : RoundDown(adjustedQuantity, symbol.QuantityDecimals.Value)) : adjustedQuantity;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a decimal value from a string
|
||||
/// </summary>
|
||||
public static decimal? ParseDecimal(string? value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value) || string.Equals("null", value, StringComparison.OrdinalIgnoreCase))
|
||||
return null;
|
||||
|
||||
if (string.Equals("Infinity", value, StringComparison.Ordinal))
|
||||
// Infinity returned by the server, default to max value
|
||||
return decimal.MaxValue;
|
||||
|
||||
try
|
||||
{
|
||||
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
// Value doesn't fit decimal, default to max value
|
||||
return decimal.MaxValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -13,9 +14,9 @@ namespace CryptoExchange.Net.Interfaces
|
||||
public interface IMessageAccessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Is this a json message
|
||||
/// Is this a valid message
|
||||
/// </summary>
|
||||
bool IsJson { get; }
|
||||
bool IsValid { get; }
|
||||
/// <summary>
|
||||
/// Is the original data available for retrieval
|
||||
/// </summary>
|
||||
@ -59,12 +60,20 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <param name="type"></param>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
CallResult<object> Deserialize(Type type, MessagePath? path = null);
|
||||
/// <summary>
|
||||
/// Deserialize the message into this type
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
CallResult<T> Deserialize<T>(MessagePath? path = null);
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,9 +1,31 @@
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Serializer interface
|
||||
/// </summary>
|
||||
public interface IMessageSerializer
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize to byte array
|
||||
/// </summary>
|
||||
public interface IByteMessageSerializer: IMessageSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Serialize an object to a string
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <returns></returns>
|
||||
byte[] Serialize<T>(T message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize to string
|
||||
/// </summary>
|
||||
public interface IStringMessageSerializer: IMessageSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Serialize an object to a string
|
||||
|
@ -78,13 +78,20 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <returns></returns>
|
||||
Task<CallResult> ConnectAsync(CancellationToken ct);
|
||||
/// <summary>
|
||||
/// Send data
|
||||
/// Send string data
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="weight"></param>
|
||||
bool Send(int id, string data, int weight);
|
||||
/// <summary>
|
||||
/// Send byte data
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="weight"></param>
|
||||
bool Send(int id, byte[] data, int weight);
|
||||
/// <summary>
|
||||
/// Reconnect the socket
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
|
@ -15,6 +15,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
private static readonly Action<ILogger, int, string?, Exception?> _webSocketError;
|
||||
private static readonly Action<ILogger, int, int, Exception?> _messageSentNotPending;
|
||||
private static readonly Action<ILogger, int, string, Exception?> _receivedData;
|
||||
private static readonly Action<ILogger, int, string, Exception?> _failedToParse;
|
||||
private static readonly Action<ILogger, int, string, Exception?> _failedToEvaluateMessage;
|
||||
private static readonly Action<ILogger, int, Exception?> _errorProcessingMessage;
|
||||
private static readonly Action<ILogger, int, int, string, Exception?> _processorMatched;
|
||||
@ -37,6 +38,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
private static readonly Action<ILogger, int, string, string, Exception?> _periodicSendFailed;
|
||||
private static readonly Action<ILogger, int, int, string, Exception?> _sendingData;
|
||||
private static readonly Action<ILogger, int, string, string, Exception?> _receivedMessageNotMatchedToAnyListener;
|
||||
private static readonly Action<ILogger, int, int, int, Exception?> _sendingByteData;
|
||||
|
||||
static SocketConnectionLoggingExtension()
|
||||
{
|
||||
@ -189,6 +191,16 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
LogLevel.Warning,
|
||||
new EventId(2029, "ReceivedMessageNotMatchedToAnyListener"),
|
||||
"[Sckt {SocketId}] received message not matched to any listener. ListenId: {ListenId}, current listeners: {ListenIds}");
|
||||
|
||||
_failedToParse = LoggerMessage.Define<int, string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(2030, "FailedToParse"),
|
||||
"[Sckt {SocketId}] failed to parse data: {Error}");
|
||||
|
||||
_sendingByteData = LoggerMessage.Define<int, int, int>(
|
||||
LogLevel.Trace,
|
||||
new EventId(2031, "SendingByteData"),
|
||||
"[Sckt {SocketId}] [Req {RequestId}] sending byte message of length: {Length}");
|
||||
}
|
||||
|
||||
public static void ActivityPaused(this ILogger logger, int socketId, bool paused)
|
||||
@ -230,6 +242,12 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
{
|
||||
_receivedData(logger, socketId, originalData, null);
|
||||
}
|
||||
|
||||
public static void FailedToParse(this ILogger logger, int socketId, string error)
|
||||
{
|
||||
_failedToParse(logger, socketId, error, null);
|
||||
}
|
||||
|
||||
public static void FailedToEvaluateMessage(this ILogger logger, int socketId, string originalData)
|
||||
{
|
||||
_failedToEvaluateMessage(logger, socketId, originalData, null);
|
||||
@ -321,5 +339,10 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
{
|
||||
_receivedMessageNotMatchedToAnyListener(logger, socketId, listenId, listenIds, null);
|
||||
}
|
||||
|
||||
public static void SendingByteData(this ILogger logger, int socketId, int requestId, int length)
|
||||
{
|
||||
_sendingByteData(logger, socketId, requestId, length, null);
|
||||
}
|
||||
}
|
||||
}
|
@ -377,7 +377,19 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
var bytes = Parameters.Encoding.GetBytes(data);
|
||||
_logger.SocketAddingBytesToSendBuffer(Id, id, bytes);
|
||||
_sendBuffer.Enqueue(new SendItem { Id = id, Weight = weight, Bytes = bytes });
|
||||
_sendBuffer.Enqueue(new SendItem { Id = id, Type = WebSocketMessageType.Text, Weight = weight, Bytes = bytes });
|
||||
_sendEvent.Set();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual bool Send(int id, byte[] data, int weight)
|
||||
{
|
||||
if (_ctsSource.IsCancellationRequested || _processState != ProcessState.Processing)
|
||||
return false;
|
||||
|
||||
_logger.SocketAddingBytesToSendBuffer(Id, id, data);
|
||||
_sendBuffer.Enqueue(new SendItem { Id = id, Type = WebSocketMessageType.Binary, Weight = weight, Bytes = data });
|
||||
_sendEvent.Set();
|
||||
return true;
|
||||
}
|
||||
@ -532,7 +544,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
try
|
||||
{
|
||||
await _socket.SendAsync(new ArraySegment<byte>(data.Bytes, 0, data.Bytes.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false);
|
||||
await _socket.SendAsync(new ArraySegment<byte>(data.Bytes, 0, data.Bytes.Length), data.Type, true, _ctsSource.Token).ConfigureAwait(false);
|
||||
await (OnRequestSent?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
_logger.SocketSentBytes(Id, data.Id, data.Bytes.Length);
|
||||
}
|
||||
@ -858,6 +870,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public DateTime SendTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Message type
|
||||
/// </summary>
|
||||
public WebSocketMessageType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The bytes to send
|
||||
/// </summary>
|
||||
|
@ -211,7 +211,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
private SocketStatus _status;
|
||||
|
||||
private readonly IMessageSerializer _serializer;
|
||||
private readonly IByteMessageAccessor _accessor;
|
||||
private IByteMessageAccessor? _stringMessageAccessor;
|
||||
private IByteMessageAccessor? _byteMessageAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// The task that is sending periodic data on the websocket. Can be used for sending Ping messages every x seconds or similar. Not necessary.
|
||||
@ -258,7 +259,6 @@ namespace CryptoExchange.Net.Sockets
|
||||
_listeners = new List<IMessageProcessor>();
|
||||
|
||||
_serializer = apiClient.CreateSerializer();
|
||||
_accessor = apiClient.CreateAccessor();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -459,25 +459,37 @@ namespace CryptoExchange.Net.Sockets
|
||||
data = ApiClient.PreprocessStreamMessage(this, type, data);
|
||||
|
||||
// 2. Read data into accessor
|
||||
_accessor.Read(data);
|
||||
IByteMessageAccessor accessor;
|
||||
if (type == WebSocketMessageType.Binary)
|
||||
accessor = _stringMessageAccessor ??= ApiClient.CreateAccessor(type);
|
||||
else
|
||||
accessor = _byteMessageAccessor ??= ApiClient.CreateAccessor(type);
|
||||
|
||||
var result = accessor.Read(data);
|
||||
try
|
||||
{
|
||||
bool outputOriginalData = ApiClient.ApiOptions.OutputOriginalData ?? ApiClient.ClientOptions.OutputOriginalData;
|
||||
if (outputOriginalData)
|
||||
{
|
||||
originalData = _accessor.GetOriginalString();
|
||||
originalData = accessor.GetOriginalString();
|
||||
_logger.ReceivedData(SocketId, originalData);
|
||||
}
|
||||
|
||||
if (!accessor.IsValid)
|
||||
{
|
||||
_logger.FailedToParse(SocketId, result.Error!.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Determine the identifying properties of this message
|
||||
var listenId = ApiClient.GetListenerIdentifier(_accessor);
|
||||
var listenId = ApiClient.GetListenerIdentifier(accessor);
|
||||
if (listenId == null)
|
||||
{
|
||||
originalData = outputOriginalData ? _accessor.GetOriginalString() : "[OutputOriginalData is false]";
|
||||
originalData = outputOriginalData ? accessor.GetOriginalString() : "[OutputOriginalData is false]";
|
||||
if (!ApiClient.UnhandledMessageExpected)
|
||||
_logger.FailedToEvaluateMessage(SocketId, originalData);
|
||||
|
||||
UnhandledMessage?.Invoke(_accessor);
|
||||
UnhandledMessage?.Invoke(accessor);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -494,7 +506,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
lock (_listenersLock)
|
||||
listenerIds = _listeners.SelectMany(l => l.ListenerIdentifiers).ToList();
|
||||
_logger.ReceivedMessageNotMatchedToAnyListener(SocketId, listenId, string.Join(",", listenerIds));
|
||||
UnhandledMessage?.Invoke(_accessor);
|
||||
UnhandledMessage?.Invoke(accessor);
|
||||
}
|
||||
|
||||
return;
|
||||
@ -512,7 +524,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
foreach (var processor in processors)
|
||||
{
|
||||
// 5. Determine the type to deserialize to for this processor
|
||||
var messageType = processor.GetMessageType(_accessor);
|
||||
var messageType = processor.GetMessageType(accessor);
|
||||
if (messageType == null)
|
||||
{
|
||||
_logger.ReceivedMessageNotRecognized(SocketId, processor.Id);
|
||||
@ -532,7 +544,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
if (deserialized == null)
|
||||
{
|
||||
var desResult = processor.Deserialize(_accessor, messageType);
|
||||
var desResult = processor.Deserialize(accessor, messageType);
|
||||
if (!desResult)
|
||||
{
|
||||
_logger.FailedToDeserializeMessage(SocketId, desResult.Error?.ToString(), desResult.Error?.Exception);
|
||||
@ -563,7 +575,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
finally
|
||||
{
|
||||
_accessor.Clear();
|
||||
accessor.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
@ -825,8 +837,55 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <param name="weight">The weight of the message</param>
|
||||
public virtual CallResult Send<T>(int requestId, T obj, int weight)
|
||||
{
|
||||
var data = obj is string str ? str : _serializer.Serialize(obj!);
|
||||
return Send(requestId, data, weight);
|
||||
if (_serializer is IByteMessageSerializer byteSerializer)
|
||||
{
|
||||
return SendBytes(requestId, byteSerializer.Serialize(obj), weight);
|
||||
}
|
||||
else if (_serializer is IStringMessageSerializer stringSerializer)
|
||||
{
|
||||
if (obj is string str)
|
||||
return Send(requestId, str, weight);
|
||||
|
||||
str = stringSerializer.Serialize(obj);
|
||||
return Send(requestId, str, weight);
|
||||
}
|
||||
|
||||
throw new Exception("Unknown serializer when sending message");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send byte data over the websocket connection
|
||||
/// </summary>
|
||||
/// <param name="data">The data to send</param>
|
||||
/// <param name="weight">The weight of the message</param>
|
||||
/// <param name="requestId">The id of the request</param>
|
||||
public virtual CallResult SendBytes(int requestId, byte[] data, int weight)
|
||||
{
|
||||
if (ApiClient.MessageSendSizeLimit != null && data.Length > ApiClient.MessageSendSizeLimit.Value)
|
||||
{
|
||||
var info = $"Message to send exceeds the max server message size ({ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit";
|
||||
_logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] {Info}", SocketId, requestId, info);
|
||||
return new CallResult(new InvalidOperationError(info));
|
||||
}
|
||||
|
||||
if (!_socket.IsOpen)
|
||||
{
|
||||
_logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] failed to send, socket no longer open", SocketId, requestId);
|
||||
return new CallResult(new WebError("Failed to send message, socket no longer open"));
|
||||
}
|
||||
|
||||
_logger.SendingByteData(SocketId, requestId, data.Length);
|
||||
try
|
||||
{
|
||||
if (!_socket.Send(requestId, data, weight))
|
||||
return new CallResult(new WebError("Failed to send message, connection not open"));
|
||||
|
||||
return CallResult.SuccessResult;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new CallResult(new WebError("Failed to send message: " + ex.Message, exception: ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -67,6 +67,17 @@ namespace CryptoExchange.Net.Testing.Implementations
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Send(int requestId, byte[] data, int weight)
|
||||
{
|
||||
if (!Connected)
|
||||
throw new Exception("Socket not connected");
|
||||
|
||||
OnRequestSent?.Invoke(requestId);
|
||||
OnMessageSend?.Invoke(Encoding.UTF8.GetString(data));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public Task CloseAsync()
|
||||
{
|
||||
Connected = false;
|
||||
|
Loading…
x
Reference in New Issue
Block a user