From 96f23f163dd8fb8ce3950f151d904c872209e00d Mon Sep 17 00:00:00 2001 From: Jan Korf Date: Mon, 14 Jul 2025 10:56:18 +0200 Subject: [PATCH] Feature/protobuf (#243) Protobuf implementation --- .../Protobuf/ProtobufMessageAccessor.cs | 530 ++++++++++++++++++ .../Protobuf/ProtobufMessageSerializer.cs | 53 ++ .../CryptoExchange.Net.Protobuf.csproj | 53 ++ .../CryptoExchange.Net.Protobuf.xml | 129 +++++ CryptoExchange.Net.Protobuf/README.md | 7 + .../TestImplementations/TestSocketClient.cs | 5 +- .../Authentication/AuthenticationProvider.cs | 7 +- CryptoExchange.Net/Clients/RestApiClient.cs | 8 +- CryptoExchange.Net/Clients/SocketApiClient.cs | 2 +- .../SystemTextJson/DecimalConverter.cs | 17 +- .../SystemTextJsonMessageAccessor.cs | 24 +- .../SystemTextJsonMessageSerializer.cs | 2 +- CryptoExchange.Net/ExchangeHelpers.cs | 24 + .../Interfaces/IMessageAccessor.cs | 13 +- .../Interfaces/IMessageSerializer.cs | 24 +- CryptoExchange.Net/Interfaces/IWebsocket.cs | 9 +- .../SocketConnectionLoggingExtension.cs | 23 + .../Sockets/CryptoExchangeWebSocketClient.cs | 21 +- .../Sockets/SocketConnection.cs | 85 ++- .../Testing/Implementations/TestSocket.cs | 11 + 20 files changed, 992 insertions(+), 55 deletions(-) create mode 100644 CryptoExchange.Net.Protobuf/Converters/Protobuf/ProtobufMessageAccessor.cs create mode 100644 CryptoExchange.Net.Protobuf/Converters/Protobuf/ProtobufMessageSerializer.cs create mode 100644 CryptoExchange.Net.Protobuf/CryptoExchange.Net.Protobuf.csproj create mode 100644 CryptoExchange.Net.Protobuf/CryptoExchange.Net.Protobuf.xml create mode 100644 CryptoExchange.Net.Protobuf/README.md diff --git a/CryptoExchange.Net.Protobuf/Converters/Protobuf/ProtobufMessageAccessor.cs b/CryptoExchange.Net.Protobuf/Converters/Protobuf/ProtobufMessageAccessor.cs new file mode 100644 index 0000000..5acccaf --- /dev/null +++ b/CryptoExchange.Net.Protobuf/Converters/Protobuf/ProtobufMessageAccessor.cs @@ -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 +{ + /// + /// System.Text.Json message accessor + /// +#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 : IMessageAccessor +#endif + { + /// + /// The intermediate deserialization object + /// + protected TIntermediateType? _intermediateType; + /// + /// Runtime type model + /// + protected RuntimeTypeModel _model; + + /// + public bool IsValid { get; set; } + + /// + public abstract bool OriginalDataAvailable { get; } + + /// + public object? Underlying => _intermediateType; + + /// + /// ctor + /// + public ProtobufMessageAccessor(RuntimeTypeModel model) + { + _model = model; + } + + /// + public NodeType? GetNodeType() + { + throw new NotImplementedException(); + } + + /// + 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); + } + + + /// +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2075:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] +#endif + public T? GetValue(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; + } + + /// + public T?[]? GetValues(MessagePath path) + { + throw new NotImplementedException(); + } + + /// + public abstract string GetOriginalString(); + + /// + public abstract void Clear(); + + /// +#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 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); + + /// +#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 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); + } + + /// + /// System.Text.Json stream message accessor + /// + 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, IStreamMessageAccessor + { + private Stream? _stream; + + /// + public override bool OriginalDataAvailable => _stream?.CanSeek == true; + + /// + /// ctor + /// + public ProtobufStreamMessageAccessor(RuntimeTypeModel model) : base(model) + { + } + + /// +#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 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(result); + } + catch (Exception ex) + { + return new CallResult(new DeserializeError(ex.Message)); + } + } + + /// +#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 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(_stream); + return new CallResult(result); + } + catch(Exception ex) + { + return new CallResult(new DeserializeError(ex.ToLogString())); + } + } + + /// + public Task 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(_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))); + } + } + + /// + 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(); + } + + /// + public override void Clear() + { + _stream?.Dispose(); + _stream = null; + _intermediateType = default; + } + + } + + /// + /// Protobuf byte message accessor + /// + 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, IByteMessageAccessor + { + private ReadOnlyMemory _bytes; + + /// + /// ctor + /// + public ProtobufByteMessageAccessor(RuntimeTypeModel model) : base(model) + { + } + + /// +#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 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(result); + } + catch (Exception ex) + { + return new CallResult(new DeserializeError(ex.ToLogString())); + } + } + + /// +#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 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 Deserialize(MessagePath? path = null) +#endif + { + try + { + var result = _model.Deserialize(_bytes); + return new CallResult(result); + } + catch (Exception ex) + { + return new CallResult(new DeserializeError(ex.Message)); + } + } + + /// + public CallResult Read(ReadOnlyMemory data) + { + _bytes = data; + + try + { + _intermediateType = _model.Deserialize(data); + IsValid = true; + return CallResult.SuccessResult; + } + catch (Exception ex) + { + // Not a json message + IsValid = false; + return new CallResult(new DeserializeError("ProtobufError: " + ex.Message, ex)); + } + } + + /// + public override string GetOriginalString() => + // NetStandard 2.0 doesn't support GetString from a ReadonlySpan, so use ToArray there instead +#if NETSTANDARD2_0 + Encoding.UTF8.GetString(_bytes.ToArray()); +#else + Encoding.UTF8.GetString(_bytes.Span); +#endif + + /// + public override bool OriginalDataAvailable => true; + + /// + public override void Clear() + { + _bytes = null; + _intermediateType = default; + } + } +} \ No newline at end of file diff --git a/CryptoExchange.Net.Protobuf/Converters/Protobuf/ProtobufMessageSerializer.cs b/CryptoExchange.Net.Protobuf/Converters/Protobuf/ProtobufMessageSerializer.cs new file mode 100644 index 0000000..80df737 --- /dev/null +++ b/CryptoExchange.Net.Protobuf/Converters/Protobuf/ProtobufMessageSerializer.cs @@ -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 +{ + /// + public class ProtobufMessageSerializer : IByteMessageSerializer + { + private RuntimeTypeModel _model; + + /// + /// ctor + /// + public ProtobufMessageSerializer(RuntimeTypeModel model) + { + _model = model; + } + + /// +#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 message) +#endif + { + using var memoryStream = new MemoryStream(); + _model.Serialize(memoryStream, message); + return memoryStream.ToArray(); + } + } +} diff --git a/CryptoExchange.Net.Protobuf/CryptoExchange.Net.Protobuf.csproj b/CryptoExchange.Net.Protobuf/CryptoExchange.Net.Protobuf.csproj new file mode 100644 index 0000000..27c8837 --- /dev/null +++ b/CryptoExchange.Net.Protobuf/CryptoExchange.Net.Protobuf.csproj @@ -0,0 +1,53 @@ + + + + netstandard2.0;netstandard2.1;net8.0;net9.0 + + + + CryptoExchange.Net.Protobuf + JKorf + Protobuf support for CryptoExchange.Net + 9.1.0 + 9.1.0 + 9.1.0 + false + CryptoExchange;CryptoExchange.Net + git + https://github.com/JKorf/CryptoExchange.Net.git + https://github.com/JKorf/CryptoExchange.Net/tree/master/CryptoExchange.Net.Protobuf + en + README.md + icon.png + true + https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes + enable + 12.0 + MIT + + + + + + + true + + + true + true + snupkg + true + true + + + CryptoExchange.Net.Protobuf.xml + + + + + + + + + + diff --git a/CryptoExchange.Net.Protobuf/CryptoExchange.Net.Protobuf.xml b/CryptoExchange.Net.Protobuf/CryptoExchange.Net.Protobuf.xml new file mode 100644 index 0000000..dcdcdca --- /dev/null +++ b/CryptoExchange.Net.Protobuf/CryptoExchange.Net.Protobuf.xml @@ -0,0 +1,129 @@ + + + + CryptoExchange.Net.Protobuf + + + + + System.Text.Json message accessor + + + + + The intermediate deserialization object + + + + + Runtime type model + + + + + + + + + + + + + + ctor + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + System.Text.Json stream message accessor + + + + + + + + ctor + + + + + + + + + + + + + + + + + + + + Protobuf byte message accessor + + + + + ctor + + + + + + + + + + + + + + + + + + + + + + + + + + ctor + + + + + + + diff --git a/CryptoExchange.Net.Protobuf/README.md b/CryptoExchange.Net.Protobuf/README.md new file mode 100644 index 0000000..35cec8f --- /dev/null +++ b/CryptoExchange.Net.Protobuf/README.md @@ -0,0 +1,7 @@ +# ![.CryptoExchange.Net](https://github.com/JKorf/CryptoExchange.Net/blob/ffcb7db8ff597c2f14982d68464015a748815580/CryptoExchange.Net/Icon/icon.png) CryptoExchange.Net.Proto + +[![.NET](https://img.shields.io/github/actions/workflow/status/JKorf/CryptoExchange.Net.Protobuf/dotnet.yml?style=for-the-badge)](https://github.com/JKorf/CryptoExchange.NetProtobuf/actions/workflows/dotnet.yml) [![Nuget downloads](https://img.shields.io/nuget/dt/CryptoExchange.NetProtobuf.svg?style=for-the-badge)](https://www.nuget.org/packages/CryptoExchange.NetProtobuf) ![License](https://img.shields.io/github/license/JKorf/CryptoExchange.Net?style=for-the-badge) + +Protobuf support for CryptoExchange.Net. + +## Release notes \ No newline at end of file diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs index 3225fbb..9df7288 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs @@ -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()); /// @@ -119,7 +120,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations public override string GetListenerIdentifier(IMessageAccessor message) { - if (!message.IsJson) + if (!message.IsValid) { return "topic"; } diff --git a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs index ede3cf0..a092078 100644 --- a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs +++ b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs @@ -465,10 +465,13 @@ namespace CryptoExchange.Net.Authentication /// protected static string GetSerializedBody(IMessageSerializer serializer, IDictionary 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); } } diff --git a/CryptoExchange.Net/Clients/RestApiClient.cs b/CryptoExchange.Net/Clients/RestApiClient.cs index 0c1b199..3d3532e 100644 --- a/CryptoExchange.Net/Clients/RestApiClient.cs +++ b/CryptoExchange.Net/Clients/RestApiClient.cs @@ -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) diff --git a/CryptoExchange.Net/Clients/SocketApiClient.cs b/CryptoExchange.Net/Clients/SocketApiClient.cs index d605818..b1a9833 100644 --- a/CryptoExchange.Net/Clients/SocketApiClient.cs +++ b/CryptoExchange.Net/Clients/SocketApiClient.cs @@ -138,7 +138,7 @@ namespace CryptoExchange.Net.Clients /// Create a message accessor instance /// /// - protected internal abstract IByteMessageAccessor CreateAccessor(); + protected internal abstract IByteMessageAccessor CreateAccessor(WebSocketMessageType messageType); /// /// Create a serializer instance diff --git a/CryptoExchange.Net/Converters/SystemTextJson/DecimalConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/DecimalConverter.cs index 7067a77..5d7b694 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/DecimalConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/DecimalConverter.cs @@ -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 diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs index dc2f3cb..0f9645c 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs @@ -24,7 +24,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson private readonly JsonSerializerOptions? _customSerializerOptions; /// - public bool IsJson { get; set; } + public bool IsValid { get; set; } /// public abstract bool OriginalDataAvailable { get; } @@ -47,7 +47,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson #endif public CallResult Deserialize(Type type, MessagePath? path = null) { - if (!IsJson) + if (!IsValid) return new CallResult(GetOriginalString()); if (_document == null) @@ -100,7 +100,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson /// 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 /// 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(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(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)); } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageSerializer.cs b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageSerializer.cs index cd654c2..f3cf340 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageSerializer.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageSerializer.cs @@ -7,7 +7,7 @@ using System.Text.Json.Serialization.Metadata; namespace CryptoExchange.Net.Converters.SystemTextJson { /// - public class SystemTextJsonMessageSerializer : IMessageSerializer + public class SystemTextJsonMessageSerializer : IStringMessageSerializer { private readonly JsonSerializerOptions _options; diff --git a/CryptoExchange.Net/ExchangeHelpers.cs b/CryptoExchange.Net/ExchangeHelpers.cs index aeb0737..29a5408 100644 --- a/CryptoExchange.Net/ExchangeHelpers.cs +++ b/CryptoExchange.Net/ExchangeHelpers.cs @@ -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; } + + /// + /// Parse a decimal value from a string + /// + 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; + } + } } } diff --git a/CryptoExchange.Net/Interfaces/IMessageAccessor.cs b/CryptoExchange.Net/Interfaces/IMessageAccessor.cs index 12c6460..1c3c28c 100644 --- a/CryptoExchange.Net/Interfaces/IMessageAccessor.cs +++ b/CryptoExchange.Net/Interfaces/IMessageAccessor.cs @@ -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 { /// - /// Is this a json message + /// Is this a valid message /// - bool IsJson { get; } + bool IsValid { get; } /// /// Is the original data available for retrieval /// @@ -59,12 +60,20 @@ namespace CryptoExchange.Net.Interfaces /// /// /// +#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 Deserialize(Type type, MessagePath? path = null); /// /// Deserialize the message into this type /// /// /// +#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 Deserialize(MessagePath? path = null); /// diff --git a/CryptoExchange.Net/Interfaces/IMessageSerializer.cs b/CryptoExchange.Net/Interfaces/IMessageSerializer.cs index 544cf93..01009a8 100644 --- a/CryptoExchange.Net/Interfaces/IMessageSerializer.cs +++ b/CryptoExchange.Net/Interfaces/IMessageSerializer.cs @@ -1,9 +1,31 @@ -namespace CryptoExchange.Net.Interfaces +using System.Diagnostics.CodeAnalysis; + +namespace CryptoExchange.Net.Interfaces { /// /// Serializer interface /// public interface IMessageSerializer + { + } + + /// + /// Serialize to byte array + /// + public interface IByteMessageSerializer: IMessageSerializer + { + /// + /// Serialize an object to a string + /// + /// + /// + byte[] Serialize(T message); + } + + /// + /// Serialize to string + /// + public interface IStringMessageSerializer: IMessageSerializer { /// /// Serialize an object to a string diff --git a/CryptoExchange.Net/Interfaces/IWebsocket.cs b/CryptoExchange.Net/Interfaces/IWebsocket.cs index 1d4c2b9..bc360d0 100644 --- a/CryptoExchange.Net/Interfaces/IWebsocket.cs +++ b/CryptoExchange.Net/Interfaces/IWebsocket.cs @@ -78,13 +78,20 @@ namespace CryptoExchange.Net.Interfaces /// Task ConnectAsync(CancellationToken ct); /// - /// Send data + /// Send string data /// /// /// /// bool Send(int id, string data, int weight); /// + /// Send byte data + /// + /// + /// + /// + bool Send(int id, byte[] data, int weight); + /// /// Reconnect the socket /// /// diff --git a/CryptoExchange.Net/Logging/Extensions/SocketConnectionLoggingExtension.cs b/CryptoExchange.Net/Logging/Extensions/SocketConnectionLoggingExtension.cs index dfd9f7c..531d180 100644 --- a/CryptoExchange.Net/Logging/Extensions/SocketConnectionLoggingExtension.cs +++ b/CryptoExchange.Net/Logging/Extensions/SocketConnectionLoggingExtension.cs @@ -15,6 +15,7 @@ namespace CryptoExchange.Net.Logging.Extensions private static readonly Action _webSocketError; private static readonly Action _messageSentNotPending; private static readonly Action _receivedData; + private static readonly Action _failedToParse; private static readonly Action _failedToEvaluateMessage; private static readonly Action _errorProcessingMessage; private static readonly Action _processorMatched; @@ -37,6 +38,7 @@ namespace CryptoExchange.Net.Logging.Extensions private static readonly Action _periodicSendFailed; private static readonly Action _sendingData; private static readonly Action _receivedMessageNotMatchedToAnyListener; + private static readonly Action _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( + LogLevel.Warning, + new EventId(2030, "FailedToParse"), + "[Sckt {SocketId}] failed to parse data: {Error}"); + + _sendingByteData = LoggerMessage.Define( + 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); + } } } \ No newline at end of file diff --git a/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs b/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs index 908a25a..f71004f 100644 --- a/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs +++ b/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs @@ -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; + } + + /// + 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(data.Bytes, 0, data.Bytes.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false); + await _socket.SendAsync(new ArraySegment(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 /// public DateTime SendTime { get; set; } + /// + /// Message type + /// + public WebSocketMessageType Type { get; set; } + /// /// The bytes to send /// diff --git a/CryptoExchange.Net/Sockets/SocketConnection.cs b/CryptoExchange.Net/Sockets/SocketConnection.cs index 72680f6..3e2d174 100644 --- a/CryptoExchange.Net/Sockets/SocketConnection.cs +++ b/CryptoExchange.Net/Sockets/SocketConnection.cs @@ -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; /// /// 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(); _serializer = apiClient.CreateSerializer(); - _accessor = apiClient.CreateAccessor(); } /// @@ -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 /// The weight of the message public virtual CallResult Send(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"); + } + + /// + /// Send byte data over the websocket connection + /// + /// The data to send + /// The weight of the message + /// The id of the request + 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)); + } } /// diff --git a/CryptoExchange.Net/Testing/Implementations/TestSocket.cs b/CryptoExchange.Net/Testing/Implementations/TestSocket.cs index b2d2f3b..5df0dae 100644 --- a/CryptoExchange.Net/Testing/Implementations/TestSocket.cs +++ b/CryptoExchange.Net/Testing/Implementations/TestSocket.cs @@ -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;