1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-08-12 17:03:10 +00:00

Compare commits

..

3 Commits

Author SHA1 Message Date
JKorf 73764970b0 . 2025-08-25 19:11:50 +02:00
JKorf 9ba29035b2 Merge branch 'master' into feature/code-analyzis 2025-08-25 17:37:46 +02:00
JKorf 4c953e2c87 wip 2025-08-24 21:58:52 +02:00
438 changed files with 24459 additions and 29940 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v1
with: with:
dotnet-version: 10.0.x dotnet-version: 9.0.x
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore run: dotnet restore
- name: Build - name: Build
@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0</TargetFrameworks> <TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<PackageId>CryptoExchange.Net.Protobuf</PackageId> <PackageId>CryptoExchange.Net.Protobuf</PackageId>
<Authors>JKorf</Authors> <Authors>JKorf</Authors>
<Description>Protobuf support for CryptoExchange.Net</Description> <Description>Protobuf support for CryptoExchange.Net</Description>
<PackageVersion>10.0.1</PackageVersion> <PackageVersion>9.6.0</PackageVersion>
<AssemblyVersion>10.0.1</AssemblyVersion> <AssemblyVersion>9.6.0</AssemblyVersion>
<FileVersion>10.0.1</FileVersion> <FileVersion>9.6.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags> <PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
@@ -41,7 +41,7 @@
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile> <DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CryptoExchange.Net" Version="10.0.2" /> <PackageReference Include="CryptoExchange.Net" Version="9.6.0" />
<PackageReference Include="protobuf-net" Version="3.2.56" /> <PackageReference Include="protobuf-net" Version="3.2.56" />
</ItemGroup> </ItemGroup>
</Project> </Project>
-30
View File
@@ -5,36 +5,6 @@
Protobuf support for CryptoExchange.Net. Protobuf support for CryptoExchange.Net.
## Release notes ## Release notes
* Version 10.0.1 - 16 Dec 2025
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 10.0.0 - 16 Dec 2025
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.13.0 - 10 Nov 2025
* Updated CryptoExchange.Net version to 9.13.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.12.0 - 03 Nov 2025
* Updated CryptoExchange.Net version to 9.12.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.11.1 - 30 Oct 2025
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.11.0 - 30 Oct 2025
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.10.0 - 15 Oct 2025
* Updated CryptoExchange.Net version to 9.10.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.9.0 - 06 Oct 2025
* Updated CryptoExchange.Net version to 9.9.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.8.0 - 30 Sep 2025
* Updated CryptoExchange.Net version to 9.8.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.7.0 - 01 Sep 2025
* Updated CryptoExchange.Net version to 9.7.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.6.0 - 25 Aug 2025 * Version 9.6.0 - 25 Aug 2025
* Updated CryptoExchange.Net version to 9.6.0 * Updated CryptoExchange.Net version to 9.6.0
@@ -4,6 +4,7 @@ using NUnit.Framework.Legacy;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests namespace CryptoExchange.Net.UnitTests
@@ -1,5 +1,11 @@
using NUnit.Framework; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.UnitTests.TestImplementations;
using Microsoft.Extensions.Logging;
using NUnit.Framework;
using NUnit.Framework.Legacy; using NUnit.Framework.Legacy;
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.UnitTests namespace CryptoExchange.Net.UnitTests
{ {
@@ -4,8 +4,10 @@ using NUnit.Framework;
using NUnit.Framework.Legacy; using NUnit.Framework.Legacy;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests namespace CryptoExchange.Net.UnitTests
{ {
@@ -111,8 +113,7 @@ namespace CryptoExchange.Net.UnitTests
{ {
var result = new WebCallResult<TestObjectResult>( var result = new WebCallResult<TestObjectResult>(
System.Net.HttpStatusCode.OK, System.Net.HttpStatusCode.OK,
HttpVersion.Version11, new KeyValuePair<string, string[]>[0],
new HttpResponseMessage().Headers,
TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1),
null, null,
"{}", "{}",
@@ -120,7 +121,7 @@ namespace CryptoExchange.Net.UnitTests
"https://test.com/api", "https://test.com/api",
null, null,
HttpMethod.Get, HttpMethod.Get,
new HttpRequestMessage().Headers, new KeyValuePair<string, string[]>[0],
ResultDataSource.Server, ResultDataSource.Server,
new TestObjectResult(), new TestObjectResult(),
null); null);
@@ -142,8 +143,7 @@ namespace CryptoExchange.Net.UnitTests
{ {
var result = new WebCallResult<TestObjectResult>( var result = new WebCallResult<TestObjectResult>(
System.Net.HttpStatusCode.OK, System.Net.HttpStatusCode.OK,
HttpVersion.Version11, new KeyValuePair<string, string[]>[0],
new HttpResponseMessage().Headers,
TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1),
null, null,
"{}", "{}",
@@ -151,7 +151,7 @@ namespace CryptoExchange.Net.UnitTests
"https://test.com/api", "https://test.com/api",
null, null,
HttpMethod.Get, HttpMethod.Get,
new HttpRequestMessage().Headers, new KeyValuePair<string, string[]>[0],
ResultDataSource.Server, ResultDataSource.Server,
new TestObjectResult(), new TestObjectResult(),
null); null);
@@ -1,15 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1"></PackageReference> <None Include="..\CryptoExchange.Net\.editorconfig" Link=".editorconfig" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"></PackageReference>
<PackageReference Include="Moq" Version="4.20.72" /> <PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="NUnit" Version="4.4.0"></PackageReference> <PackageReference Include="NUnit" Version="4.3.2"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="6.0.1"></PackageReference> <PackageReference Include="NUnit3TestAdapter" Version="5.0.0"></PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -1,5 +1,7 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using NUnit.Framework; using NUnit.Framework;
using NUnit.Framework.Legacy;
using System.Diagnostics;
using System.Globalization; using System.Globalization;
namespace CryptoExchange.Net.UnitTests namespace CryptoExchange.Net.UnitTests
@@ -30,7 +32,6 @@ namespace CryptoExchange.Net.UnitTests
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.532, 0.532)] [TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.532, 0.532)]
[TestCase(0.1, 1, 0.0001, RoundingType.Down, 0.5516592, 0.5516)] [TestCase(0.1, 1, 0.0001, RoundingType.Down, 0.5516592, 0.5516)]
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.5516592, 0.5517)] [TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.5516592, 0.5517)]
[TestCase(0, 1, 0.000000001, RoundingType.Closest, 0.0000097232, 0.000009723)]
public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected) public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected)
{ {
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input); var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
@@ -1,8 +1,15 @@
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.UnitTests.TestImplementations; using CryptoExchange.Net.UnitTests.TestImplementations;
using Microsoft.Extensions.Logging;
using NUnit.Framework; using NUnit.Framework;
using NUnit.Framework.Legacy;
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests namespace CryptoExchange.Net.UnitTests
{ {
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
using System.Threading; using System.Threading;
using NUnit.Framework.Legacy; using NUnit.Framework.Legacy;
using CryptoExchange.Net.RateLimiting; using CryptoExchange.Net.RateLimiting;
using System.Net;
using CryptoExchange.Net.RateLimiting.Guards; using CryptoExchange.Net.RateLimiting.Guards;
using CryptoExchange.Net.RateLimiting.Filters; using CryptoExchange.Net.RateLimiting.Filters;
using CryptoExchange.Net.RateLimiting.Interfaces; using CryptoExchange.Net.RateLimiting.Interfaces;
+200 -203
View File
@@ -1,234 +1,231 @@
//using CryptoExchange.Net.Objects; using System;
//using CryptoExchange.Net.Objects.Sockets; using System.Collections.Generic;
//using CryptoExchange.Net.Sockets; using System.Text.Json;
//using CryptoExchange.Net.Testing.Implementations; using System.Threading;
//using CryptoExchange.Net.UnitTests.TestImplementations; using System.Threading.Tasks;
//using CryptoExchange.Net.UnitTests.TestImplementations.Sockets; using CryptoExchange.Net.Objects;
//using Microsoft.Extensions.Logging; using CryptoExchange.Net.Objects.Sockets;
//using Moq; using CryptoExchange.Net.Sockets;
//using NUnit.Framework; using CryptoExchange.Net.UnitTests.TestImplementations;
//using NUnit.Framework.Legacy; using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
//using System; using Microsoft.Extensions.Logging;
//using System.Collections.Generic; using Moq;
//using System.Net.Sockets; using NUnit.Framework;
//using System.Text.Json; using NUnit.Framework.Legacy;
//using System.Threading;
//using System.Threading.Tasks;
//namespace CryptoExchange.Net.UnitTests namespace CryptoExchange.Net.UnitTests
//{ {
// [TestFixture] [TestFixture]
// public class SocketClientTests public class SocketClientTests
// { {
// [TestCase] [TestCase]
// public void SettingOptions_Should_ResultInOptionsSet() public void SettingOptions_Should_ResultInOptionsSet()
// { {
// //arrange //arrange
// //act //act
// var client = new TestSocketClient(options => var client = new TestSocketClient(options =>
// { {
// options.SubOptions.ApiCredentials = new Authentication.ApiCredentials("1", "2"); options.SubOptions.ApiCredentials = new Authentication.ApiCredentials("1", "2");
// options.SubOptions.MaxSocketConnections = 1; options.SubOptions.MaxSocketConnections = 1;
// }); });
// //assert //assert
// ClassicAssert.NotNull(client.SubClient.ApiOptions.ApiCredentials); ClassicAssert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
// Assert.That(1 == client.SubClient.ApiOptions.MaxSocketConnections); Assert.That(1 == client.SubClient.ApiOptions.MaxSocketConnections);
// } }
// [TestCase(true)] [TestCase(true)]
// [TestCase(false)] [TestCase(false)]
// public void ConnectSocket_Should_ReturnConnectionResult(bool canConnect) public void ConnectSocket_Should_ReturnConnectionResult(bool canConnect)
// { {
// //arrange //arrange
// var client = new TestSocketClient(); var client = new TestSocketClient();
// var socket = client.CreateSocket(); var socket = client.CreateSocket();
// socket.CanConnect = canConnect; socket.CanConnect = canConnect;
// //act //act
// var connectResult = client.SubClient.ConnectSocketSub( var connectResult = client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, null));
// new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
// //assert //assert
// Assert.That(connectResult.Success == canConnect); Assert.That(connectResult.Success == canConnect);
// } }
// [TestCase] [TestCase]
// public void SocketMessages_Should_BeProcessedInDataHandlers() public void SocketMessages_Should_BeProcessedInDataHandlers()
// { {
// // arrange // arrange
// var client = new TestSocketClient(options => { var client = new TestSocketClient(options => {
// options.ReconnectInterval = TimeSpan.Zero; options.ReconnectInterval = TimeSpan.Zero;
// }); });
// var socket = client.CreateSocket(); var socket = client.CreateSocket();
// socket.CanConnect = true; socket.CanConnect = true;
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""); var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
// var rstEvent = new ManualResetEvent(false); var rstEvent = new ManualResetEvent(false);
// Dictionary<string, string> result = null; Dictionary<string, string> result = null;
// client.SubClient.ConnectSocketSub(sub); client.SubClient.ConnectSocketSub(sub);
// var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
// { {
// result = messageEvent.Data; result = messageEvent.Data;
// rstEvent.Set(); rstEvent.Set();
// }); });
// sub.AddSubscription(subObj); sub.AddSubscription(subObj);
// // act // act
// socket.InvokeMessage("{\"property\": \"123\", \"action\": \"update\", \"topic\": \"topic\"}"); socket.InvokeMessage("{\"property\": \"123\", \"action\": \"update\", \"topic\": \"topic\"}");
// rstEvent.WaitOne(1000); rstEvent.WaitOne(1000);
// // assert // assert
// Assert.That(result["property"] == "123"); Assert.That(result["property"] == "123");
// } }
// [TestCase(false)] [TestCase(false)]
// [TestCase(true)] [TestCase(true)]
// public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled) public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
// { {
// // arrange // arrange
// var client = new TestSocketClient(options => var client = new TestSocketClient(options =>
// { {
// options.ReconnectInterval = TimeSpan.Zero; options.ReconnectInterval = TimeSpan.Zero;
// options.SubOptions.OutputOriginalData = enabled; options.SubOptions.OutputOriginalData = enabled;
// }); });
// var socket = client.CreateSocket(); var socket = client.CreateSocket();
// socket.CanConnect = true; socket.CanConnect = true;
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""); var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
// var rstEvent = new ManualResetEvent(false); var rstEvent = new ManualResetEvent(false);
// string original = null; string original = null;
// client.SubClient.ConnectSocketSub(sub); client.SubClient.ConnectSocketSub(sub);
// var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
// { {
// original = messageEvent.OriginalData; original = messageEvent.OriginalData;
// rstEvent.Set(); rstEvent.Set();
// }); });
// sub.AddSubscription(subObj); sub.AddSubscription(subObj);
// var msgToSend = JsonSerializer.Serialize(new { topic = "topic", action = "update", property = "123" }); var msgToSend = JsonSerializer.Serialize(new { topic = "topic", action = "update", property = "123" });
// // act // act
// socket.InvokeMessage(msgToSend); socket.InvokeMessage(msgToSend);
// rstEvent.WaitOne(1000); rstEvent.WaitOne(1000);
// // assert // assert
// Assert.That(original == (enabled ? msgToSend : null)); Assert.That(original == (enabled ? msgToSend : null));
// } }
// [TestCase()] [TestCase()]
// public void UnsubscribingStream_Should_CloseTheSocket() public void UnsubscribingStream_Should_CloseTheSocket()
// { {
// // arrange // arrange
// var client = new TestSocketClient(options => var client = new TestSocketClient(options =>
// { {
// options.ReconnectInterval = TimeSpan.Zero; options.ReconnectInterval = TimeSpan.Zero;
// }); });
// var socket = client.CreateSocket(); var socket = client.CreateSocket();
// socket.CanConnect = true; socket.CanConnect = true;
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""); var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
// client.SubClient.ConnectSocketSub(sub); client.SubClient.ConnectSocketSub(sub);
// var subscription = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { }); var subscription = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
// var ups = new UpdateSubscription(sub, subscription); var ups = new UpdateSubscription(sub, subscription);
// sub.AddSubscription(subscription); sub.AddSubscription(subscription);
// // act // act
// client.UnsubscribeAsync(ups).Wait(); client.UnsubscribeAsync(ups).Wait();
// // assert // assert
// Assert.That(socket.Connected == false); Assert.That(socket.Connected == false);
// } }
// [TestCase()] [TestCase()]
// public void UnsubscribingAll_Should_CloseAllSockets() public void UnsubscribingAll_Should_CloseAllSockets()
// { {
// // arrange // arrange
// var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; }); var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
// var socket1 = client.CreateSocket(); var socket1 = client.CreateSocket();
// var socket2 = client.CreateSocket(); var socket2 = client.CreateSocket();
// socket1.CanConnect = true; socket1.CanConnect = true;
// socket2.CanConnect = true; socket2.CanConnect = true;
// var sub1 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket1), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""); var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket1, null);
// var sub2 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket2), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""); var sub2 = new SocketConnection(new TraceLogger(), client.SubClient, socket2, null);
// client.SubClient.ConnectSocketSub(sub1); client.SubClient.ConnectSocketSub(sub1);
// client.SubClient.ConnectSocketSub(sub2); client.SubClient.ConnectSocketSub(sub2);
// var subscription1 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { }); var subscription1 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
// var subscription2 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { }); var subscription2 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
// sub1.AddSubscription(subscription1); sub1.AddSubscription(subscription1);
// sub2.AddSubscription(subscription2); sub2.AddSubscription(subscription2);
// var ups1 = new UpdateSubscription(sub1, subscription1); var ups1 = new UpdateSubscription(sub1, subscription1);
// var ups2 = new UpdateSubscription(sub2, subscription2); var ups2 = new UpdateSubscription(sub2, subscription2);
// // act // act
// client.UnsubscribeAllAsync().Wait(); client.UnsubscribeAllAsync().Wait();
// // assert // assert
// Assert.That(socket1.Connected == false); Assert.That(socket1.Connected == false);
// Assert.That(socket2.Connected == false); Assert.That(socket2.Connected == false);
// } }
// [TestCase()] [TestCase()]
// public void FailingToConnectSocket_Should_ReturnError() public void FailingToConnectSocket_Should_ReturnError()
// { {
// // arrange // arrange
// var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; }); var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
// var socket = client.CreateSocket(); var socket = client.CreateSocket();
// socket.CanConnect = false; socket.CanConnect = false;
// var sub1 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""); var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
// // act // act
// var connectResult = client.SubClient.ConnectSocketSub(sub1); var connectResult = client.SubClient.ConnectSocketSub(sub1);
// // assert // assert
// ClassicAssert.IsFalse(connectResult.Success); ClassicAssert.IsFalse(connectResult.Success);
// } }
// [TestCase()] [TestCase()]
// public async Task ErrorResponse_ShouldNot_ConfirmSubscription() public async Task ErrorResponse_ShouldNot_ConfirmSubscription()
// { {
// // arrange // arrange
// var channel = "trade_btcusd"; var channel = "trade_btcusd";
// var client = new TestSocketClient(opt => var client = new TestSocketClient(opt =>
// { {
// opt.OutputOriginalData = true; opt.OutputOriginalData = true;
// opt.SocketSubscriptionsCombineTarget = 1; opt.SocketSubscriptionsCombineTarget = 1;
// }); });
// var socket = client.CreateSocket(); var socket = client.CreateSocket();
// socket.CanConnect = true; socket.CanConnect = true;
// client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "")); client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, "https://test.test"));
// // act // act
// var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default); var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
// socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "error" })); socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "error" }));
// await sub; await sub;
// // assert // assert
// ClassicAssert.IsTrue(client.SubClient.TestSubscription.Status != SubscriptionStatus.Subscribed); ClassicAssert.IsFalse(client.SubClient.TestSubscription.Confirmed);
// } }
// [TestCase()] [TestCase()]
// public async Task SuccessResponse_Should_ConfirmSubscription() public async Task SuccessResponse_Should_ConfirmSubscription()
// { {
// // arrange // arrange
// var channel = "trade_btcusd"; var channel = "trade_btcusd";
// var client = new TestSocketClient(opt => var client = new TestSocketClient(opt =>
// { {
// opt.OutputOriginalData = true; opt.OutputOriginalData = true;
// opt.SocketSubscriptionsCombineTarget = 1; opt.SocketSubscriptionsCombineTarget = 1;
// }); });
// var socket = client.CreateSocket(); var socket = client.CreateSocket();
// socket.CanConnect = true; socket.CanConnect = true;
// client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "")); client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, "https://test.test"));
// // act // act
// var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default); var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
// socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "confirmed" })); socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "confirmed" }));
// await sub; await sub;
// // assert // assert
// Assert.That(client.SubClient.TestSubscription.Status == SubscriptionStatus.Subscribed); Assert.That(client.SubClient.TestSubscription.Confirmed);
// } }
// } }
//} }
@@ -4,7 +4,9 @@ using System.Text.Json;
using NUnit.Framework; using NUnit.Framework;
using System; using System;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using NUnit.Framework.Legacy;
using CryptoExchange.Net.Converters; using CryptoExchange.Net.Converters;
using CryptoExchange.Net.Testing.Comparers;
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
namespace CryptoExchange.Net.UnitTests namespace CryptoExchange.Net.UnitTests
@@ -0,0 +1,50 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{
internal class SubResponse
{
[JsonPropertyName("action")]
public string Action { get; set; } = null!;
[JsonPropertyName("channel")]
public string Channel { get; set; } = null!;
[JsonPropertyName("status")]
public string Status { get; set; } = null!;
}
internal class UnsubResponse
{
[JsonPropertyName("action")]
public string Action { get; set; } = null!;
[JsonPropertyName("status")]
public string Status { get; set; } = null!;
}
internal class TestChannelQuery : Query<SubResponse>
{
public TestChannelQuery(string channel, string request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
{
MessageMatcher = MessageMatcher.Create<SubResponse>(request + "-" + channel, HandleMessage);
}
public CallResult<SubResponse> HandleMessage(SocketConnection connection, DataEvent<SubResponse> message)
{
if (!message.Data.Status.Equals("confirmed", StringComparison.OrdinalIgnoreCase))
{
return new CallResult<SubResponse>(new ServerError(ErrorInfo.Unknown with { Message = message.Data.Status }));
}
return message.ToCallResult();
}
}
}
@@ -0,0 +1,17 @@
using CryptoExchange.Net.Sockets;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{
internal class TestQuery : Query<object>
{
public TestQuery(string identifier, object request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
{
MessageMatcher = MessageMatcher.Create<object>(identifier);
}
}
}
@@ -0,0 +1,34 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{
internal class TestSubscription<T> : Subscription<object, object>
{
private readonly Action<DataEvent<T>> _handler;
public TestSubscription(ILogger logger, Action<DataEvent<T>> handler) : base(logger, false)
{
_handler = handler;
MessageMatcher = MessageMatcher.Create<T>("update-topic", DoHandleMessage);
}
public CallResult DoHandleMessage(SocketConnection connection, DataEvent<T> message)
{
_handler.Invoke(message);
return new CallResult(null);
}
protected override Query GetSubQuery(SocketConnection connection) => new TestQuery("sub", new object(), false, 1);
protected override Query GetUnsubQuery(SocketConnection connection) => new TestQuery("unsub", new object(), false, 1);
}
}
@@ -0,0 +1,34 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using Microsoft.Extensions.Logging;
using Moq;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{
internal class TestSubscriptionWithResponseCheck<T> : Subscription<SubResponse, UnsubResponse>
{
private readonly Action<DataEvent<T>> _handler;
private readonly string _channel;
public TestSubscriptionWithResponseCheck(string channel, Action<DataEvent<T>> handler) : base(Mock.Of<ILogger>(), false)
{
MessageMatcher = MessageMatcher.Create<T>(channel, DoHandleMessage);
_handler = handler;
_channel = channel;
}
public CallResult DoHandleMessage(SocketConnection connection, DataEvent<T> message)
{
_handler.Invoke(message);
return new CallResult(null);
}
protected override Query GetSubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "subscribe", false, 1);
protected override Query GetUnsubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "unsubscribe", false, 1);
}
}
@@ -1,16 +1,19 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Net.Http;
using System.Text; using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Clients; using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Converters.SystemTextJson; using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors; using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.UnitTests.TestImplementations;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
@@ -43,8 +46,6 @@ namespace CryptoExchange.Net.UnitTests
public class TestSubClient : RestApiClient public class TestSubClient : RestApiClient
{ {
protected override IRestMessageHandler MessageHandler => throw new NotImplementedException();
public TestSubClient(RestExchangeOptions<TestEnvironment> options, RestApiOptions apiOptions) : base(new TraceLogger(), null, "https://localhost:123", options, apiOptions) public TestSubClient(RestExchangeOptions<TestEnvironment> options, RestApiOptions apiOptions) : base(new TraceLogger(), null, "https://localhost:123", options, apiOptions)
{ {
} }
@@ -63,6 +64,8 @@ namespace CryptoExchange.Net.UnitTests
/// <inheritdoc /> /// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}"; public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
public override TimeSpan? GetTimeOffset() => null;
public override TimeSyncInfo GetTimeSyncInfo() => null;
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions()); protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions());
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions()); protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException(); protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
@@ -71,8 +74,6 @@ namespace CryptoExchange.Net.UnitTests
public class TestAuthProvider : AuthenticationProvider public class TestAuthProvider : AuthenticationProvider
{ {
public override ApiCredentialsType[] SupportedCredentialTypes => [ApiCredentialsType.Hmac];
public TestAuthProvider(ApiCredentials credentials) : base(credentials) public TestAuthProvider(ApiCredentials credentials) : base(credentials)
{ {
} }
@@ -84,14 +85,4 @@ namespace CryptoExchange.Net.UnitTests
public string GetKey() => _credentials.Key; public string GetKey() => _credentials.Key;
public string GetSecret() => _credentials.Secret; public string GetSecret() => _credentials.Secret;
} }
public class TestEnvironment : TradeEnvironment
{
public string TestAddress { get; }
public TestEnvironment(string name, string url) : base(name)
{
TestAddress = url;
}
}
} }
@@ -13,13 +13,12 @@ using CryptoExchange.Net.Authentication;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using CryptoExchange.Net.Clients; using CryptoExchange.Net.Clients;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using System.Linq; using System.Linq;
using CryptoExchange.Net.Converters.SystemTextJson; using CryptoExchange.Net.Converters.SystemTextJson;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Net.Http.Headers; using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
namespace CryptoExchange.Net.UnitTests.TestImplementations namespace CryptoExchange.Net.UnitTests.TestImplementations
{ {
@@ -50,19 +49,19 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
var response = new Mock<IResponse>(); var response = new Mock<IResponse>();
response.Setup(c => c.IsSuccessStatusCode).Returns(true); response.Setup(c => c.IsSuccessStatusCode).Returns(true);
response.Setup(c => c.GetResponseStreamAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult((Stream)responseStream)); response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
var headers = new HttpRequestMessage().Headers; var headers = new Dictionary<string, string[]>();
var request = new Mock<IRequest>(); var request = new Mock<IRequest>();
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com")); request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object)); request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
request.Setup(c => c.SetContent(It.IsAny<string>(), It.IsAny<string>())).Callback(new Action<string, string>((content, type) => { request.Setup(r => r.Content).Returns(content); })); request.Setup(c => c.SetContent(It.IsAny<string>(), It.IsAny<string>())).Callback(new Action<string, string>((content, type) => { request.Setup(r => r.Content).Returns(content); }));
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new string[] { val })); request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new string[] { val }));
request.Setup(c => c.GetHeaders()).Returns(() => headers); request.Setup(c => c.GetHeaders()).Returns(() => headers.ToArray());
var factory = Mock.Get(Api1.RequestFactory); var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>())) factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => .Callback<HttpMethod, Uri, int>((method, uri, id) =>
{ {
request.Setup(a => a.Uri).Returns(uri); request.Setup(a => a.Uri).Returns(uri);
request.Setup(a => a.Method).Returns(method); request.Setup(a => a.Method).Returns(method);
@@ -70,8 +69,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
.Returns(request.Object); .Returns(request.Object);
factory = Mock.Get(Api2.RequestFactory); factory = Mock.Get(Api2.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>())) factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => .Callback<HttpMethod, Uri, int>((method, uri, id) =>
{ {
request.Setup(a => a.Uri).Returns(uri); request.Setup(a => a.Uri).Returns(uri);
request.Setup(a => a.Method).Returns(method); request.Setup(a => a.Method).Returns(method);
@@ -87,16 +86,16 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
var request = new Mock<IRequest>(); var request = new Mock<IRequest>();
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com")); request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
request.Setup(c => c.GetHeaders()).Returns(new HttpRequestMessage().Headers); request.Setup(c => c.GetHeaders()).Returns(new KeyValuePair<string, string[]>[0]);
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we); request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
var factory = Mock.Get(Api1.RequestFactory); var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>())) factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Returns(request.Object); .Returns(request.Object);
factory = Mock.Get(Api2.RequestFactory); factory = Mock.Get(Api2.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>())) factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Returns(request.Object); .Returns(request.Object);
} }
@@ -109,31 +108,29 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
var response = new Mock<IResponse>(); var response = new Mock<IResponse>();
response.Setup(c => c.IsSuccessStatusCode).Returns(false); response.Setup(c => c.IsSuccessStatusCode).Returns(false);
response.Setup(c => c.GetResponseStreamAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult((Stream)responseStream)); response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
var headers = new List<KeyValuePair<string, string[]>>(); var headers = new List<KeyValuePair<string, string[]>>();
var request = new Mock<IRequest>(); var request = new Mock<IRequest>();
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com")); request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object)); request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(new KeyValuePair<string, string[]>(key, new string[] { val }))); request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(new KeyValuePair<string, string[]>(key, new string[] { val })));
request.Setup(c => c.GetHeaders()).Returns(new HttpRequestMessage().Headers); request.Setup(c => c.GetHeaders()).Returns(headers.ToArray());
var factory = Mock.Get(Api1.RequestFactory); var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>())) factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri)) .Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
.Returns(request.Object); .Returns(request.Object);
factory = Mock.Get(Api2.RequestFactory); factory = Mock.Get(Api2.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>())) factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri)) .Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
.Returns(request.Object); .Returns(request.Object);
} }
} }
public class TestRestApi1Client : RestApiClient public class TestRestApi1Client : RestApiClient
{ {
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
public TestRestApi1Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api1Options) public TestRestApi1Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api1Options)
{ {
RequestFactory = new Mock<IRequestFactory>().Object; RequestFactory = new Mock<IRequestFactory>().Object;
@@ -160,6 +157,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
ParameterPositions[method] = position; ParameterPositions[method] = position;
} }
public override TimeSpan? GetTimeOffset()
{
throw new NotImplementedException();
}
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
=> new TestAuthProvider(credentials); => new TestAuthProvider(credentials);
@@ -167,12 +169,15 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public override TimeSyncInfo GetTimeSyncInfo()
{
throw new NotImplementedException();
}
} }
public class TestRestApi2Client : RestApiClient public class TestRestApi2Client : RestApiClient
{ {
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
public TestRestApi2Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api2Options) public TestRestApi2Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api2Options)
{ {
RequestFactory = new Mock<IRequestFactory>().Object; RequestFactory = new Mock<IRequestFactory>().Object;
@@ -189,6 +194,18 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct); return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
} }
protected override Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor, Exception exception)
{
var errorData = accessor.Deserialize<TestError>();
return new ServerError(errorData.Data.ErrorCode, GetErrorInfo(errorData.Data.ErrorCode, errorData.Data.ErrorMessage));
}
public override TimeSpan? GetTimeOffset()
{
throw new NotImplementedException();
}
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
=> new TestAuthProvider(credentials); => new TestAuthProvider(credentials);
@@ -197,6 +214,10 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
throw new NotImplementedException(); throw new NotImplementedException();
} }
public override TimeSyncInfo GetTimeSyncInfo()
{
throw new NotImplementedException();
}
} }
public class TestError public class TestError
@@ -1,29 +0,0 @@
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
internal class TestRestMessageHandler : JsonRestMessageHandler
{
private ErrorMapping _errorMapping = new ErrorMapping([]);
public override JsonSerializerOptions Options => new JsonSerializerOptions();
public override ValueTask<Error> ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream)
{
var errorData = JsonSerializer.Deserialize<TestError>(responseStream);
return new ValueTask<Error>(new ServerError(errorData.ErrorCode, _errorMapping.GetErrorInfo(errorData.ErrorCode.ToString(), errorData.ErrorMessage)));
}
}
}
@@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Converters.MessageParsing;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
using Microsoft.Extensions.Logging;
using Moq;
using CryptoExchange.Net.Testing.Implementations;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Options;
using CryptoExchange.Net.Converters.SystemTextJson;
using System.Net.WebSockets;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
internal class TestSocketClient: BaseSocketClient
{
public TestSubSocketClient SubClient { get; }
/// <summary>
/// Create a new instance of KucoinSocketClient
/// </summary>
/// <param name="optionsFunc">Configure the options to use for this client</param>
public TestSocketClient(Action<TestSocketOptions> optionsDelegate = null)
: this(Options.Create(ApplyOptionsDelegate(optionsDelegate)), null)
{
}
public TestSocketClient(IOptions<TestSocketOptions> options, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
{
Initialize(options.Value);
SubClient = AddApiClient(new TestSubSocketClient(options.Value, options.Value.SubOptions));
SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object;
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com"));
}
public TestSocket CreateSocket()
{
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com"));
return (TestSocket)SubClient.CreateSocketInternal("https://localhost:123/");
}
}
public class TestEnvironment : TradeEnvironment
{
public string TestAddress { get; }
public TestEnvironment(string name, string url) : base(name)
{
TestAddress = url;
}
}
public class TestSocketOptions: SocketExchangeOptions<TestEnvironment>
{
public static TestSocketOptions Default = new TestSocketOptions
{
Environment = new TestEnvironment("Live", "https://test.test")
};
/// <summary>
/// ctor
/// </summary>
public TestSocketOptions()
{
Default?.Set(this);
}
public SocketApiOptions SubOptions { get; set; } = new SocketApiOptions();
internal TestSocketOptions Set(TestSocketOptions targetOptions)
{
targetOptions = base.Set<TestSocketOptions>(targetOptions);
targetOptions.SubOptions = SubOptions.Set(targetOptions.SubOptions);
return targetOptions;
}
}
public class TestSubSocketClient : SocketApiClient
{
private MessagePath _channelPath = MessagePath.Get().Property("channel");
private MessagePath _actionPath = MessagePath.Get().Property("action");
private MessagePath _topicPath = MessagePath.Get().Property("topic");
public Subscription TestSubscription { get; private set; } = null;
public TestSubSocketClient(TestSocketOptions options, SocketApiOptions apiOptions) : base(new TraceLogger(), options.Environment.TestAddress, options, apiOptions)
{
}
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 />
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
internal IWebsocket CreateSocketInternal(string address)
{
return CreateSocket(address);
}
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
=> new TestAuthProvider(credentials);
public CallResult ConnectSocketSub(SocketConnection sub)
{
return ConnectSocketAsync(sub, default).Result;
}
public override string GetListenerIdentifier(IMessageAccessor message)
{
if (!message.IsValid)
{
return "topic";
}
var id = message.GetValue<string>(_channelPath);
id ??= message.GetValue<string>(_topicPath);
return message.GetValue<string>(_actionPath) + "-" + id;
}
public Task<CallResult<UpdateSubscription>> SubscribeToSomethingAsync(string channel, Action<DataEvent<string>> onUpdate, CancellationToken ct)
{
TestSubscription = new TestSubscriptionWithResponseCheck<string>(channel, onUpdate);
return SubscribeAsync(TestSubscription, ct);
}
}
}
@@ -1,6 +1,10 @@
using CryptoExchange.Net.UnitTests.TestImplementations; using CryptoExchange.Net.UnitTests.TestImplementations;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests namespace CryptoExchange.Net.UnitTests
{ {
+183
View File
@@ -0,0 +1,183 @@
root = true
[*]
# Indentation and spacing
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
charset = utf-8
max_line_length = 140
insert_final_newline = true
# ReSharper code style properties
resharper_csharp_keep_existing_embedded_arrangement = false
resharper_csharp_place_accessorholder_attribute_on_same_line = false
resharper_csharp_wrap_after_declaration_lpar = true
resharper_csharp_wrap_parameters_style = chop_if_long
resharper_csharp_blank_lines_around_single_line_auto_property = 1
resharper_csharp_keep_blank_lines_in_declarations = 1
resharper_trailing_comma_in_multiline_lists = true
[*.cs]
indent_size = 4
# Code style conventions
dotnet_style_predefined_type_for_member_access = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_object_initializer = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_expression_bodied_methods = true:suggestion
csharp_style_namespace_declarations = file_scoped:warning
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
csharp_prefer_braces = when_multiline:warning
# Analyzer preferences
dotnet_diagnostic.CA2007.severity = warning # Call ConfigureAwait on the awaited Task.
dotnet_code_quality.CA2007.exclude_async_void_methods = true
dotnet_code_quality.CA2007.output_kind = DynamicallyLinkedLibrary
dotnet_diagnostic.CA1000.severity = none # Do not declare static members on generic types
dotnet_diagnostic.CA1051.severity = none # Do not declare visible instance fields
dotnet_diagnostic.CA1510.severity = none # Use ArgumentNullException throw helper
dotnet_diagnostic.CA1720.severity = none # Identifiers should not contain type names
dotnet_diagnostic.CA1716.severity = none # Identifiers should not match keywords
dotnet_diagnostic.CA1835.severity = none # Use ArgumentNullException throw helper
dotnet_diagnostic.CA1846.severity = none # Prefer AsSpan over Substring
dotnet_diagnostic.CA1848.severity = none # Use the LoggerMessage delegates
dotnet_diagnostic.CA1850.severity = none # Prefer static HashData method over ComputeHash
dotnet_diagnostic.CA1866.severity = none # Use 'string.Method(char)' instead of 'string.Method(string)' for string with single char
dotnet_diagnostic.CA2201.severity = none # Do not raise reserved exception types
dotnet_diagnostic.CA2208.severity = none # Do not raise reserved exception types
dotnet_diagnostic.IDE0005.severity = warning # Using directive is unnecessary
[*.xml]
ij_xml_space_inside_empty_tag = true
[*.cs]
#### Naming styles ####
# Naming rules
dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = warning
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.private_or_internal_field_should_be_fields_start_with__.severity = warning
dotnet_naming_rule.private_or_internal_field_should_be_fields_start_with__.symbols = private_or_internal_field
dotnet_naming_rule.private_or_internal_field_should_be_fields_start_with__.style = fields_start_with__
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field
dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected
dotnet_naming_symbols.private_or_internal_field.required_modifiers =
# Naming styles
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.fields_start_with__.required_prefix = _
dotnet_naming_style.fields_start_with__.required_suffix =
dotnet_naming_style.fields_start_with__.word_separator =
dotnet_naming_style.fields_start_with__.capitalization = camel_case
csharp_indent_labels = one_less_than_current
csharp_using_directive_placement = outside_namespace:suggestion
csharp_prefer_simple_using_statement = true:suggestion
csharp_style_prefer_method_group_conversion = true:silent
csharp_style_prefer_top_level_statements = true:silent
csharp_style_prefer_primary_constructors = true:suggestion
csharp_prefer_system_threading_lock = true:suggestion
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:suggestion
csharp_style_expression_bodied_indexers = true:suggestion
csharp_style_expression_bodied_accessors = true:suggestion
csharp_style_expression_bodied_lambdas = true:silent
csharp_style_expression_bodied_local_functions = true:silent
[*.vb]
#### Naming styles ####
# Naming rules
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
[*.{cs,vb}]
#### Naming styles ####
# Naming rules
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
# Symbol specifications
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
# Naming styles
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_style_operator_placement_when_wrapping = beginning_of_line
tab_width = 4
end_of_line = crlf
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+4 -5
View File
@@ -1,6 +1,5 @@
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")]
namespace System.Runtime.CompilerServices namespace System.Runtime.CompilerServices;
{
internal static class IsExternalInit { } internal static class IsExternalInit { }
}
@@ -1,12 +1,11 @@
using System; using System;
namespace CryptoExchange.Net.Attributes namespace CryptoExchange.Net.Attributes;
/// <summary>
/// Used for conversion in ArrayConverter
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class JsonConversionAttribute: Attribute
{ {
/// <summary>
/// Used for conversion in ArrayConverter
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class JsonConversionAttribute: Attribute
{
}
} }
+17 -18
View File
@@ -1,25 +1,24 @@
using System; using System;
namespace CryptoExchange.Net.Attributes namespace CryptoExchange.Net.Attributes;
/// <summary>
/// Map a enum entry to string values
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class MapAttribute : Attribute
{ {
/// <summary> /// <summary>
/// Map a enum entry to string values /// Values mapping to the enum entry
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Field)] public string[] Values { get; set; }
public class MapAttribute : Attribute
{
/// <summary>
/// Values mapping to the enum entry
/// </summary>
public string[] Values { get; set; }
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="maps"></param> /// <param name="maps"></param>
public MapAttribute(params string[] maps) public MapAttribute(params string[] maps)
{ {
Values = maps; Values = maps;
}
} }
} }
@@ -1,101 +1,56 @@
using System; using System;
using System.IO;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Authentication namespace CryptoExchange.Net.Authentication;
/// <summary>
/// Api credentials, used to sign requests accessing private endpoints
/// </summary>
public class ApiCredentials
{ {
/// <summary> /// <summary>
/// Api credentials, used to sign requests accessing private endpoints /// The api key / label to authenticate requests
/// </summary> /// </summary>
public class ApiCredentials public string Key { get; set; }
/// <summary>
/// The api secret or private key to authenticate requests
/// </summary>
public string Secret { get; set; }
/// <summary>
/// The api passphrase. Not needed on all exchanges
/// </summary>
public string? Pass { get; set; }
/// <summary>
/// Type of the credentials
/// </summary>
public ApiCredentialsType CredentialType { get; set; }
/// <summary>
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
/// <param name="key">The api key / label used for identification</param>
/// <param name="secret">The api secret or private key used for signing</param>
/// <param name="pass">The api pass for the key. Not always needed</param>
/// <param name="credentialType">The type of credentials</param>
public ApiCredentials(string key, string secret, string? pass = null, ApiCredentialsType credentialType = ApiCredentialsType.Hmac)
{ {
/// <summary> if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
/// The api key / label to authenticate requests throw new ArgumentException("Key and secret can't be null/empty");
/// </summary>
public string Key { get; set; }
/// <summary> CredentialType = credentialType;
/// The api secret or private key to authenticate requests Key = key;
/// </summary> Secret = secret;
public string Secret { get; set; } Pass = pass;
}
/// <summary> /// <summary>
/// The api passphrase. Not needed on all exchanges /// Copy the credentials
/// </summary> /// </summary>
public string? Pass { get; set; } /// <returns></returns>
public virtual ApiCredentials Copy()
/// <summary> {
/// Type of the credentials return new ApiCredentials(Key, Secret, Pass, CredentialType);
/// </summary>
public ApiCredentialsType CredentialType { get; set; }
/// <summary>
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
/// <param name="key">The api key / label used for identification</param>
/// <param name="secret">The api secret or private key used for signing</param>
/// <param name="pass">The api pass for the key. Not always needed</param>
/// <param name="credentialType">The type of credentials</param>
public ApiCredentials(string key, string secret, string? pass = null, ApiCredentialsType credentialType = ApiCredentialsType.Hmac)
{
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
throw new ArgumentException("Key and secret can't be null/empty");
CredentialType = credentialType;
Key = key;
Secret = secret;
Pass = pass;
}
/// <summary>
/// Create API credentials using an API key and secret generated by the server
/// </summary>
public static ApiCredentials HmacCredentials(string apiKey, string apiSecret, string? pass)
{
return new ApiCredentials(apiKey, apiSecret, pass, ApiCredentialsType.Hmac);
}
/// <summary>
/// Create API credentials using an API key and an RSA private key in PEM format
/// </summary>
public static ApiCredentials RsaPemCredentials(string apiKey, string privateKey)
{
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.RsaPem);
}
/// <summary>
/// Create API credentials using an API key and an RSA private key in XML format
/// </summary>
public static ApiCredentials RsaXmlCredentials(string apiKey, string privateKey)
{
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.RsaXml);
}
/// <summary>
/// Create API credentials using an API key and an Ed25519 private key
/// </summary>
public static ApiCredentials Ed25519Credentials(string apiKey, string privateKey)
{
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.Ed25519);
}
/// <summary>
/// Load a key from a file
/// </summary>
public static string ReadFromFile(string path)
{
using var fileStream = File.OpenRead(path);
using var streamReader = new StreamReader(fileStream);
return streamReader.ReadToEnd();
}
/// <summary>
/// Copy the credentials
/// </summary>
/// <returns></returns>
public virtual ApiCredentials Copy()
{
return new ApiCredentials(Key, Secret, Pass, CredentialType);
}
} }
} }
@@ -1,25 +1,20 @@
namespace CryptoExchange.Net.Authentication namespace CryptoExchange.Net.Authentication;
/// <summary>
/// Credentials type
/// </summary>
public enum ApiCredentialsType
{ {
/// <summary> /// <summary>
/// Credentials type /// Hmac keys credentials
/// </summary> /// </summary>
public enum ApiCredentialsType Hmac,
{ /// <summary>
/// <summary> /// Rsa keys credentials in xml format
/// Hmac keys credentials /// </summary>
/// </summary> RsaXml,
Hmac, /// <summary>
/// <summary> /// Rsa keys credentials in pem/base64 format. Only available for .NetStandard 2.1 and up, use xml format for lower.
/// Rsa keys credentials in xml format /// </summary>
/// </summary> RsaPem
RsaXml,
/// <summary>
/// Rsa keys credentials in pem/base64 format. Only available for .NetStandard 2.1 and up, use xml format for lower.
/// </summary>
RsaPem,
/// <summary>
/// Ed25519 keys credentials
/// </summary>
Ed25519
}
} }
File diff suppressed because it is too large Load Diff
@@ -1,17 +1,16 @@
namespace CryptoExchange.Net.Authentication namespace CryptoExchange.Net.Authentication;
/// <summary>
/// Output string type
/// </summary>
public enum SignOutputType
{ {
/// <summary> /// <summary>
/// Output string type /// Hex string
/// </summary> /// </summary>
public enum SignOutputType Hex,
{ /// <summary>
/// <summary> /// Base64 string
/// Hex string /// </summary>
/// </summary> Base64
Hex,
/// <summary>
/// Base64 string
/// </summary>
Base64
}
} }
+42 -48
View File
@@ -1,58 +1,52 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Linq; using System.Linq;
using System.Threading;
namespace CryptoExchange.Net.Caching namespace CryptoExchange.Net.Caching;
internal class MemoryCache
{ {
internal class MemoryCache private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
private readonly object _lock = new object();
/// <summary>
/// Add a new cache entry. Will override an existing entry if it already exists
/// </summary>
/// <param name="key">The key identifier</param>
/// <param name="value">Cache value</param>
public void Add(string key, object value)
{ {
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>(); var cacheItem = new CacheItem(DateTime.UtcNow, value);
#if NET9_0_OR_GREATER _cache.AddOrUpdate(key, cacheItem, (key, val1) => cacheItem);
private readonly Lock _lock = new Lock(); }
#else
private readonly object _lock = new object();
#endif
/// <summary> /// <summary>
/// Add a new cache entry. Will override an existing entry if it already exists /// Get a cached value
/// </summary> /// </summary>
/// <param name="key">The key identifier</param> /// <param name="key">The key identifier</param>
/// <param name="value">Cache value</param> /// <param name="maxAge">The max age of the cached entry</param>
public void Add(string key, object value) /// <returns>Cached value if it was in cache</returns>
public object? Get(string key, TimeSpan maxAge)
{
foreach (var item in _cache.Where(x => DateTime.UtcNow - x.Value.CacheTime > maxAge).ToList())
_cache.TryRemove(item.Key, out _);
_cache.TryGetValue(key, out CacheItem? value);
if (value == null)
return null;
return value.Value;
}
private class CacheItem
{
public DateTime CacheTime { get; }
public object Value { get; }
public CacheItem(DateTime cacheTime, object value)
{ {
var cacheItem = new CacheItem(DateTime.UtcNow, value); CacheTime = cacheTime;
_cache.AddOrUpdate(key, cacheItem, (key, val1) => cacheItem); Value = value;
}
/// <summary>
/// Get a cached value
/// </summary>
/// <param name="key">The key identifier</param>
/// <param name="maxAge">The max age of the cached entry</param>
/// <returns>Cached value if it was in cache</returns>
public object? Get(string key, TimeSpan maxAge)
{
foreach (var item in _cache.Where(x => DateTime.UtcNow - x.Value.CacheTime > maxAge).ToList())
_cache.TryRemove(item.Key, out _);
_cache.TryGetValue(key, out CacheItem? value);
if (value == null)
return null;
return value.Value;
}
private class CacheItem
{
public DateTime CacheTime { get; }
public object Value { get; }
public CacheItem(DateTime cacheTime, object value)
{
CacheTime = cacheTime;
Value = value;
}
} }
} }
} }
+116 -125
View File
@@ -1,149 +1,140 @@
using System; using System;
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces.Clients; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects.Errors; using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Clients namespace CryptoExchange.Net.Clients;
/// <summary>
/// Base API for all API clients
/// </summary>
public abstract class BaseApiClient : IDisposable, IBaseApiClient
{ {
/// <summary> /// <summary>
/// Base API for all API clients /// Logger
/// </summary> /// </summary>
public abstract class BaseApiClient : IDisposable, IBaseApiClient protected ILogger _logger;
/// <summary>
/// If we are disposing
/// </summary>
protected bool _disposing;
/// <summary>
/// The authentication provider for this API client. (null if no credentials are set)
/// </summary>
public AuthenticationProvider? AuthenticationProvider { get; private set; }
/// <summary>
/// The environment this client communicates to
/// </summary>
public string BaseAddress { get; }
/// <summary>
/// Output the original string data along with the deserialized object
/// </summary>
public bool OutputOriginalData { get; }
/// <inheritdoc />
public bool Authenticated => ApiCredentials != null;
/// <inheritdoc />
public ApiCredentials? ApiCredentials { get; set; }
/// <summary>
/// Api options
/// </summary>
public ApiOptions ApiOptions { get; }
/// <summary>
/// Client Options
/// </summary>
public ExchangeOptions ClientOptions { get; }
/// <summary>
/// Mapping of a response code to known error types
/// </summary>
protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]);
/// <summary>
/// ctor
/// </summary>
/// <param name="logger">Logger</param>
/// <param name="outputOriginalData">Should data from this client include the original data in the call result</param>
/// <param name="baseAddress">Base address for this API client</param>
/// <param name="apiCredentials">Api credentials</param>
/// <param name="clientOptions">Client options</param>
/// <param name="apiOptions">Api options</param>
protected BaseApiClient(ILogger logger, bool outputOriginalData, ApiCredentials? apiCredentials, string baseAddress, ExchangeOptions clientOptions, ApiOptions apiOptions)
{ {
private string? _clientName; _logger = logger;
/// <summary> ClientOptions = clientOptions;
/// Logger ApiOptions = apiOptions;
/// </summary> OutputOriginalData = outputOriginalData;
protected ILogger _logger; BaseAddress = baseAddress;
ApiCredentials = apiCredentials?.Copy();
/// <summary> if (ApiCredentials != null)
/// If we are disposing AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
/// </summary> }
protected bool _disposing;
/// <summary> /// <summary>
/// Name of the client /// Create an AuthenticationProvider implementation instance based on the provided credentials
/// </summary> /// </summary>
protected internal string ClientName /// <param name="credentials"></param>
{ /// <returns></returns>
get protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
{
if (_clientName != null)
return _clientName;
_clientName = GetType().Name; /// <inheritdoc />
return _clientName; public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
}
}
/// <summary> /// <summary>
/// The authentication provider for this API client. (null if no credentials are set) /// Get error info for a response code
/// </summary> /// </summary>
public AuthenticationProvider? AuthenticationProvider { get; private set; } public ErrorInfo GetErrorInfo(int code, string? message = null) => GetErrorInfo(code.ToString(), message);
/// <summary> /// <summary>
/// The environment this client communicates to /// Get error info for a response code
/// </summary> /// </summary>
public string BaseAddress { get; } public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message);
/// <summary> /// <inheritdoc />
/// Output the original string data along with the deserialized object public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
/// </summary> {
public bool OutputOriginalData { get; } ApiCredentials = credentials?.Copy();
if (ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
}
/// <inheritdoc /> /// <inheritdoc />
public bool Authenticated => ApiCredentials != null; public virtual void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials
{
ClientOptions.Proxy = options.Proxy;
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
/// <inheritdoc /> ApiCredentials = options.ApiCredentials?.Copy() ?? ApiCredentials;
public ApiCredentials? ApiCredentials { get; set; } if (ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
}
/// <summary> /// <summary>
/// Api options /// Dispose
/// </summary> /// </summary>
public ApiOptions ApiOptions { get; } public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary> /// <summary>
/// Client Options /// Dispose
/// </summary> /// </summary>
public ExchangeOptions ClientOptions { get; } public virtual void Dispose(bool disposing)
{
/// <summary> _disposing = true;
/// Mapping of a response code to known error types
/// </summary>
protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]);
/// <summary>
/// ctor
/// </summary>
/// <param name="logger">Logger</param>
/// <param name="outputOriginalData">Should data from this client include the original data in the call result</param>
/// <param name="baseAddress">Base address for this API client</param>
/// <param name="apiCredentials">Api credentials</param>
/// <param name="clientOptions">Client options</param>
/// <param name="apiOptions">Api options</param>
protected BaseApiClient(ILogger logger, bool outputOriginalData, ApiCredentials? apiCredentials, string baseAddress, ExchangeOptions clientOptions, ApiOptions apiOptions)
{
_logger = logger;
ClientOptions = clientOptions;
ApiOptions = apiOptions;
OutputOriginalData = outputOriginalData;
BaseAddress = baseAddress;
ApiCredentials = apiCredentials?.Copy();
if (ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
}
/// <summary>
/// Create an AuthenticationProvider implementation instance based on the provided credentials
/// </summary>
/// <param name="credentials"></param>
/// <returns></returns>
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
/// <inheritdoc />
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
/// <summary>
/// Get error info for a response code
/// </summary>
public ErrorInfo GetErrorInfo(int code, string? message = null) => GetErrorInfo(code.ToString(), message);
/// <summary>
/// Get error info for a response code
/// </summary>
public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message);
/// <inheritdoc />
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
{
ApiCredentials = credentials?.Copy();
if (ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
}
/// <inheritdoc />
public virtual void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials
{
ClientOptions.Proxy = options.Proxy;
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
ApiCredentials = options.ApiCredentials?.Copy() ?? ApiCredentials;
if (ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
}
/// <summary>
/// Dispose
/// </summary>
public virtual void Dispose()
{
_disposing = true;
}
} }
} }
+114 -104
View File
@@ -1,132 +1,142 @@
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
namespace CryptoExchange.Net.Clients namespace CryptoExchange.Net.Clients;
/// <summary>
/// The base for all clients, websocket client and rest client
/// </summary>
public abstract class BaseClient : IDisposable
{ {
/// <summary> /// <summary>
/// The base for all clients, websocket client and rest client /// Version of the CryptoExchange.Net base library
/// </summary> /// </summary>
public abstract class BaseClient : IDisposable public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version!;
/// <summary>
/// Version of the client implementation
/// </summary>
public Version ExchangeLibVersion
{ {
/// <summary> get
/// Version of the CryptoExchange.Net base library
/// </summary>
public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version!;
/// <summary>
/// Version of the client implementation
/// </summary>
public Version ExchangeLibVersion
{ {
get lock(_versionLock)
{ {
lock(_versionLock) if (_exchangeVersion == null)
{ _exchangeVersion = GetType().Assembly.GetName().Version!;
if (_exchangeVersion == null)
_exchangeVersion = GetType().Assembly.GetName().Version!;
return _exchangeVersion; return _exchangeVersion;
}
} }
} }
}
/// <summary> /// <summary>
/// The name of the API the client is for /// The name of the API the client is for
/// </summary> /// </summary>
public string Exchange { get; } public string Exchange { get; }
/// <summary> /// <summary>
/// Api clients in this client /// Api clients in this client
/// </summary> /// </summary>
internal List<BaseApiClient> ApiClients { get; } = new List<BaseApiClient>(); internal List<BaseApiClient> ApiClients { get; } = new List<BaseApiClient>();
/// <summary> /// <summary>
/// The log object /// The log object
/// </summary> /// </summary>
protected internal ILogger _logger; protected internal ILogger _logger;
#if NET9_0_OR_GREATER private readonly object _versionLock = new object();
private readonly Lock _versionLock = new Lock(); private Version _exchangeVersion;
#else
private readonly object _versionLock = new object();
#endif
private Version _exchangeVersion;
/// <summary> /// <summary>
/// Provided client options /// Provided client options
/// </summary> /// </summary>
public ExchangeOptions ClientOptions { get; private set; } public ExchangeOptions ClientOptions { get; private set; }
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="logger">Logger</param> /// <param name="logger">Logger</param>
/// <param name="exchange">The name of the exchange this client is for</param> /// <param name="exchange">The name of the exchange this client is for</param>
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
protected BaseClient(ILoggerFactory? logger, string exchange) protected BaseClient(ILoggerFactory? logger, string exchange)
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
{
Exchange = exchange;
}
/// <summary>
/// Initialize the client with the specified options
/// </summary>
/// <param name="options"></param>
/// <exception cref="ArgumentNullException"></exception>
protected virtual void Initialize(ExchangeOptions options)
{
if (options == null)
throw new ArgumentNullException(nameof(options));
ClientOptions = options;
_logger.Log(LogLevel.Trace, "Client configuration: {Options}, CryptoExchange.Net: v{CryptoExchangeVersion}, {Exchange}.Net: v{ExchangeVersion}", options, CryptoExchangeLibVersion, Exchange, ExchangeLibVersion);
}
/// <summary>
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
/// </summary>
/// <param name="credentials">The credentials to set</param>
protected virtual void SetApiCredentials<T>(T credentials) where T : ApiCredentials
{
foreach (var apiClient in ApiClients)
apiClient.SetApiCredentials(credentials);
}
/// <summary>
/// Register an API client
/// </summary>
/// <param name="apiClient">The client</param>
protected T AddApiClient<T>(T apiClient) where T : BaseApiClient
{
if (ClientOptions == null)
throw new InvalidOperationException("Client should have called Initialize before adding API clients");
_logger.Log(LogLevel.Trace, " {ApiClient}, base address: {BaseAddress}", apiClient.GetType().Name, apiClient.BaseAddress);
ApiClients.Add(apiClient);
return apiClient;
}
/// <summary>
/// Apply the options delegate to a new options instance
/// </summary>
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T: new()
{
var opts = new T();
del?.Invoke(opts);
return opts;
}
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose
/// </summary>
public virtual void Dispose(bool disposing)
{
if (disposing)
{ {
Exchange = exchange; _logger.Log(LogLevel.Debug, "Disposing client");
}
/// <summary>
/// Initialize the client with the specified options
/// </summary>
/// <param name="options"></param>
/// <exception cref="ArgumentNullException"></exception>
protected virtual void Initialize(ExchangeOptions options)
{
if (options == null)
throw new ArgumentNullException(nameof(options));
ClientOptions = options;
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}");
}
/// <summary>
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
/// </summary>
/// <param name="credentials">The credentials to set</param>
protected virtual void SetApiCredentials<T>(T credentials) where T : ApiCredentials
{
foreach (var apiClient in ApiClients)
apiClient.SetApiCredentials(credentials);
}
/// <summary>
/// Register an API client
/// </summary>
/// <param name="apiClient">The client</param>
protected T AddApiClient<T>(T apiClient) where T : BaseApiClient
{
if (ClientOptions == null)
throw new InvalidOperationException("Client should have called Initialize before adding API clients");
ApiClients.Add(apiClient);
return apiClient;
}
/// <summary>
/// Apply the options delegate to a new options instance
/// </summary>
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T: new()
{
var opts = new T();
del?.Invoke(opts);
return opts;
}
/// <summary>
/// Dispose
/// </summary>
public virtual void Dispose()
{
foreach (var client in ApiClients) foreach (var client in ApiClients)
client.Dispose(); client.Dispose();
} }
} }
} }
+15 -18
View File
@@ -1,28 +1,25 @@
using System.Linq; using System.Linq;
using CryptoExchange.Net.Interfaces.Clients; using CryptoExchange.Net.Interfaces;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
namespace CryptoExchange.Net.Clients namespace CryptoExchange.Net.Clients;
/// <summary>
/// Base rest client
/// </summary>
public abstract class BaseRestClient : BaseClient, IRestClient
{ {
/// <inheritdoc />
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
/// <summary> /// <summary>
/// Base rest client /// ctor
/// </summary> /// </summary>
public abstract class BaseRestClient : BaseClient, IRestClient /// <param name="loggerFactory">Logger factory</param>
/// <param name="name">The name of the API this client is for</param>
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
{ {
/// <inheritdoc /> _logger = loggerFactory?.CreateLogger(name + ".RestClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
/// <summary>
/// ctor
/// </summary>
/// <param name="loggerFactory">Logger factory</param>
/// <param name="name">The name of the API this client is for</param>
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
{
_logger = loggerFactory?.CreateLogger(name + ".RestClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
}
} }
} }
+115 -122
View File
@@ -1,137 +1,130 @@
using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace CryptoExchange.Net.Clients namespace CryptoExchange.Net.Clients;
/// <summary>
/// Base for socket client implementations
/// </summary>
public abstract class BaseSocketClient : BaseClient, ISocketClient
{ {
#region fields
/// <summary> /// <summary>
/// Base for socket client implementations /// If client is disposing
/// </summary> /// </summary>
public abstract class BaseSocketClient : BaseClient, ISocketClient protected bool _disposing;
/// <inheritdoc />
public int CurrentConnections => ApiClients.OfType<SocketApiClient>().Sum(c => c.CurrentConnections);
/// <inheritdoc />
public int CurrentSubscriptions => ApiClients.OfType<SocketApiClient>().Sum(s => s.CurrentSubscriptions);
/// <inheritdoc />
public double IncomingKbps => ApiClients.OfType<SocketApiClient>().Sum(s => s.IncomingKbps);
#endregion
/// <summary>
/// ctor
/// </summary>
/// <param name="loggerFactory">Logger factory</param>
/// <param name="name">The name of the exchange this client is for</param>
protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
{ {
#region fields _logger = loggerFactory?.CreateLogger(name + ".SocketClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
}
/// <summary> /// <summary>
/// If client is disposing /// Unsubscribe an update subscription
/// </summary> /// </summary>
protected bool _disposing; /// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
/// <returns></returns>
/// <inheritdoc /> public virtual async Task UnsubscribeAsync(int subscriptionId)
public int CurrentConnections => ApiClients.OfType<SocketApiClient>().Sum(c => c.CurrentConnections); {
/// <inheritdoc /> foreach (var socket in ApiClients.OfType<SocketApiClient>())
public int CurrentSubscriptions => ApiClients.OfType<SocketApiClient>().Sum(s => s.CurrentSubscriptions);
/// <inheritdoc />
public double IncomingKbps => ApiClients.OfType<SocketApiClient>().Sum(s => s.IncomingKbps);
/// <inheritdoc />
public new SocketExchangeOptions ClientOptions => (SocketExchangeOptions)base.ClientOptions;
#endregion
/// <summary>
/// ctor
/// </summary>
/// <param name="loggerFactory">Logger factory</param>
/// <param name="name">The name of the exchange this client is for</param>
protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
{ {
_logger = loggerFactory?.CreateLogger(name + ".SocketClient") ?? NullLoggerFactory.Instance.CreateLogger(name); var result = await socket.UnsubscribeAsync(subscriptionId).ConfigureAwait(false);
if (result)
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange"); break;
}
/// <summary>
/// Unsubscribe an update subscription
/// </summary>
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
/// <returns></returns>
public virtual async Task UnsubscribeAsync(int subscriptionId)
{
foreach (var socket in ApiClients.OfType<SocketApiClient>())
{
var result = await socket.UnsubscribeAsync(subscriptionId).ConfigureAwait(false);
if (result)
break;
}
}
/// <summary>
/// Unsubscribe an update subscription
/// </summary>
/// <param name="subscription">The subscription to unsubscribe</param>
/// <returns></returns>
public virtual async Task UnsubscribeAsync(UpdateSubscription subscription)
{
if (subscription == null)
throw new ArgumentNullException(nameof(subscription));
_logger.UnsubscribingSubscription(subscription.SocketId, subscription.Id);
await subscription.CloseAsync().ConfigureAwait(false);
}
/// <summary>
/// Unsubscribe all subscriptions
/// </summary>
/// <returns></returns>
public virtual async Task UnsubscribeAllAsync()
{
var tasks = new List<Task>();
foreach (var client in ApiClients.OfType<SocketApiClient>())
tasks.Add(client.UnsubscribeAllAsync());
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
}
/// <summary>
/// Reconnect all connections
/// </summary>
/// <returns></returns>
public virtual async Task ReconnectAsync()
{
_logger.ReconnectingAllConnections(CurrentConnections);
var tasks = new List<Task>();
foreach (var client in ApiClients.OfType<SocketApiClient>())
{
tasks.Add(client.ReconnectAsync());
}
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
}
/// <summary>
/// Log the current state of connections and subscriptions
/// </summary>
public string GetSubscriptionsState()
{
var result = new StringBuilder();
foreach (var client in ApiClients.OfType<SocketApiClient>().Where(c => c.CurrentSubscriptions > 0))
{
result.AppendLine(client.GetSubscriptionsState());
}
return result.ToString();
}
/// <summary>
/// Returns the state of all socket api clients
/// </summary>
/// <returns></returns>
public List<SocketApiClient.SocketApiClientState> GetSocketApiClientStates()
{
var result = new List<SocketApiClient.SocketApiClientState>();
foreach (var client in ApiClients.OfType<SocketApiClient>())
{
result.Add(client.GetState());
}
return result;
} }
} }
/// <summary>
/// Unsubscribe an update subscription
/// </summary>
/// <param name="subscription">The subscription to unsubscribe</param>
/// <returns></returns>
public virtual async Task UnsubscribeAsync(UpdateSubscription subscription)
{
if (subscription == null)
throw new ArgumentNullException(nameof(subscription));
_logger.UnsubscribingSubscription(subscription.SocketId, subscription.Id);
await subscription.CloseAsync().ConfigureAwait(false);
}
/// <summary>
/// Unsubscribe all subscriptions
/// </summary>
/// <returns></returns>
public virtual async Task UnsubscribeAllAsync()
{
var tasks = new List<Task>();
foreach (var client in ApiClients.OfType<SocketApiClient>())
tasks.Add(client.UnsubscribeAllAsync());
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
}
/// <summary>
/// Reconnect all connections
/// </summary>
/// <returns></returns>
public virtual async Task ReconnectAsync()
{
_logger.ReconnectingAllConnections(CurrentConnections);
var tasks = new List<Task>();
foreach (var client in ApiClients.OfType<SocketApiClient>())
{
tasks.Add(client.ReconnectAsync());
}
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
}
/// <summary>
/// Log the current state of connections and subscriptions
/// </summary>
public string GetSubscriptionsState()
{
var result = new StringBuilder();
foreach (var client in ApiClients.OfType<SocketApiClient>().Where(c => c.CurrentSubscriptions > 0))
{
result.AppendLine(client.GetSubscriptionsState());
}
return result.ToString();
}
/// <summary>
/// Returns the state of all socket api clients
/// </summary>
/// <returns></returns>
public List<SocketApiClient.SocketApiClientState> GetSocketApiClientStates()
{
var result = new List<SocketApiClient.SocketApiClientState>();
foreach (var client in ApiClients.OfType<SocketApiClient>())
{
result.Add(client.GetState());
}
return result;
}
} }
+60 -49
View File
@@ -1,67 +1,78 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace CryptoExchange.Net.Clients namespace CryptoExchange.Net.Clients;
/// <summary>
/// Base crypto client
/// </summary>
public class CryptoBaseClient : IDisposable
{ {
private readonly Dictionary<Type, object> _serviceCache = new Dictionary<Type, object>();
/// <summary> /// <summary>
/// Base crypto client /// Service provider
/// </summary> /// </summary>
public class CryptoBaseClient : IDisposable protected readonly IServiceProvider? _serviceProvider;
/// <summary>
/// ctor
/// </summary>
public CryptoBaseClient() { }
/// <summary>
/// ctor
/// </summary>
/// <param name="serviceProvider"></param>
public CryptoBaseClient(IServiceProvider serviceProvider)
{ {
private readonly Dictionary<Type, object> _serviceCache = new Dictionary<Type, object>(); _serviceProvider = serviceProvider;
_serviceCache = new Dictionary<Type, object>();
}
/// <summary> /// <summary>
/// Service provider /// Try get a client by type for the service collection
/// </summary> /// </summary>
protected readonly IServiceProvider? _serviceProvider; /// <typeparam name="T"></typeparam>
/// <returns></returns>
public T TryGet<T>(Func<T> createFunc)
{
var type = typeof(T);
if (_serviceCache.TryGetValue(type, out var value))
return (T)value;
/// <summary> if (_serviceProvider == null)
/// ctor
/// </summary>
public CryptoBaseClient() { }
/// <summary>
/// ctor
/// </summary>
/// <param name="serviceProvider"></param>
public CryptoBaseClient(IServiceProvider serviceProvider)
{ {
_serviceProvider = serviceProvider; // Create with default options
_serviceCache = new Dictionary<Type, object>(); var createResult = createFunc();
_serviceCache.Add(typeof(T), createResult!);
return createResult;
} }
/// <summary> var result = _serviceProvider.GetService<T>()
/// Try get a client by type for the service collection ?? throw new InvalidOperationException($"No service was found for {typeof(T).Name}, make sure the exchange is registered in dependency injection with the `services.Add[Exchange]()` method");
/// </summary> _serviceCache.Add(type, result!);
/// <typeparam name="T"></typeparam> return result;
/// <returns></returns> }
public T TryGet<T>(Func<T> createFunc)
{
var type = typeof(T);
if (_serviceCache.TryGetValue(type, out var value))
return (T)value;
if (_serviceProvider == null) /// <summary>
{ /// Dispose
// Create with default options /// </summary>
var createResult = createFunc(); public void Dispose(bool disposing)
_serviceCache.Add(typeof(T), createResult!); {
return createResult; if (disposing)
}
var result = _serviceProvider.GetService<T>()
?? throw new InvalidOperationException($"No service was found for {typeof(T).Name}, make sure the exchange is registered in dependency injection with the `services.Add[Exchange]()` method");
_serviceCache.Add(type, result!);
return result;
}
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{ {
_serviceCache.Clear(); _serviceCache.Clear();
} }
} }
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
} }
+18 -19
View File
@@ -1,24 +1,23 @@
using CryptoExchange.Net.Interfaces.Clients; using CryptoExchange.Net.Interfaces;
using System; using System;
namespace CryptoExchange.Net.Clients namespace CryptoExchange.Net.Clients;
{
/// <inheritdoc />
public class CryptoRestClient : CryptoBaseClient, ICryptoRestClient
{
/// <summary>
/// ctor
/// </summary>
public CryptoRestClient()
{
}
/// <summary> /// <inheritdoc />
/// ctor public class CryptoRestClient : CryptoBaseClient, ICryptoRestClient
/// </summary> {
/// <param name="serviceProvider"></param> /// <summary>
public CryptoRestClient(IServiceProvider serviceProvider) : base(serviceProvider) /// ctor
{ /// </summary>
} public CryptoRestClient()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="serviceProvider"></param>
public CryptoRestClient(IServiceProvider serviceProvider) : base(serviceProvider)
{
} }
} }
@@ -1,24 +1,23 @@
using CryptoExchange.Net.Interfaces.Clients; using CryptoExchange.Net.Interfaces;
using System; using System;
namespace CryptoExchange.Net.Clients namespace CryptoExchange.Net.Clients;
{
/// <inheritdoc />
public class CryptoSocketClient : CryptoBaseClient, ICryptoSocketClient
{
/// <summary>
/// ctor
/// </summary>
public CryptoSocketClient()
{
}
/// <summary> /// <inheritdoc />
/// ctor public class CryptoSocketClient : CryptoBaseClient, ICryptoSocketClient
/// </summary> {
/// <param name="serviceProvider"></param> /// <summary>
public CryptoSocketClient(IServiceProvider serviceProvider) : base(serviceProvider) /// ctor
{ /// </summary>
} public CryptoSocketClient()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="serviceProvider"></param>
public CryptoSocketClient(IServiceProvider serviceProvider) : base(serviceProvider)
{
} }
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,25 +1,24 @@
using System; using System;
namespace CryptoExchange.Net.Converters namespace CryptoExchange.Net.Converters;
/// <summary>
/// Mark property as an index in the array
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class ArrayPropertyAttribute : Attribute
{ {
/// <summary> /// <summary>
/// Mark property as an index in the array /// The index in the array
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Property)] public int Index { get; }
public class ArrayPropertyAttribute : Attribute
{
/// <summary>
/// The index in the array
/// </summary>
public int Index { get; }
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="index"></param> /// <param name="index"></param>
public ArrayPropertyAttribute(int index) public ArrayPropertyAttribute(int index)
{ {
Index = index; Index = index;
}
} }
} }
@@ -1,29 +1,28 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters namespace CryptoExchange.Net.Converters;
/// <summary>
/// Caching for JsonSerializerContext instances
/// </summary>
public static class JsonSerializerContextCache
{ {
private static ConcurrentDictionary<Type, JsonSerializerContext> _cache = new ConcurrentDictionary<Type, JsonSerializerContext>();
/// <summary> /// <summary>
/// Caching for JsonSerializerContext instances /// Get the instance of the provided type T. It will be created if it doesn't exist yet.
/// </summary> /// </summary>
public static class JsonSerializerContextCache /// <typeparam name="T">Implementation type of the JsonSerializerContext</typeparam>
public static JsonSerializerContext GetOrCreate<T>() where T: JsonSerializerContext, new()
{ {
private static ConcurrentDictionary<Type, JsonSerializerContext> _cache = new ConcurrentDictionary<Type, JsonSerializerContext>(); var contextType = typeof(T);
if (_cache.TryGetValue(contextType, out var context))
return context;
/// <summary> var instance = new T();
/// Get the instance of the provided type T. It will be created if it doesn't exist yet. _cache[contextType] = instance;
/// </summary> return instance;
/// <typeparam name="T">Implementation type of the JsonSerializerContext</typeparam>
public static JsonSerializerContext GetOrCreate<T>() where T: JsonSerializerContext, new()
{
var contextType = typeof(T);
if (_cache.TryGetValue(contextType, out var context))
return context;
var instance = new T();
_cache[contextType] = instance;
return instance;
}
} }
} }
@@ -1,64 +0,0 @@
using CryptoExchange.Net.Objects;
using System.IO;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// REST message handler
/// </summary>
public interface IRestMessageHandler
{
/// <summary>
/// The `accept` HTTP response header for the request
/// </summary>
MediaTypeWithQualityHeaderValue AcceptHeader { get; }
/// <summary>
/// Whether a seekable stream is required
/// </summary>
bool RequiresSeekableStream { get; }
/// <summary>
/// Parse the response when the HTTP response status indicated an error
/// </summary>
ValueTask<Error> ParseErrorResponse(
int httpStatusCode,
HttpResponseHeaders responseHeaders,
Stream responseStream);
/// <summary>
/// Parse the response when the HTTP response status indicated a rate limit error
/// </summary>
ValueTask<ServerRateLimitError> ParseErrorRateLimitResponse(
int httpStatusCode,
HttpResponseHeaders responseHeaders,
Stream responseStream);
/// <summary>
/// Check if the response is an error response; if so return the error.<br />
/// Note that if the API returns a standard result wrapper, something like this:
/// <code>{ "code": 400, "msg": "error", "data": {} }</code>
/// then the `CheckDeserializedResponse` method should be used for checking the result
/// </summary>
ValueTask<Error?> CheckForErrorResponse(
RequestDefinition request,
HttpResponseHeaders responseHeaders,
Stream responseStream);
/// <summary>
/// Deserialize the response stream
/// </summary>
ValueTask<(T? Result, Error? Error)> TryDeserializeAsync<T>(
Stream responseStream,
CancellationToken ct);
/// <summary>
/// Check whether the resulting T object indicates an error or not
/// </summary>
Error? CheckDeserializedResponse<T>(HttpResponseHeaders responseHeaders, T result);
}
}
@@ -1,27 +0,0 @@
using System;
using System.Net.WebSockets;
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// WebSocket message handler
/// </summary>
public interface ISocketMessageHandler
{
/// <summary>
/// Get an identifier for the message which can be used to determine the type of the message
/// </summary>
string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType);
/// <summary>
/// Get optional topic filter, for example a symbol name
/// </summary>
string? GetTopicFilter(object deserializedObject);
/// <summary>
/// Deserialize to the provided type
/// </summary>
object Deserialize(ReadOnlySpan<byte> data, Type type);
}
}
@@ -1,46 +0,0 @@
using System;
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// Message type definition
/// </summary>
public class MessageTypeDefinition
{
/// <summary>
/// Whether to immediately select the definition when it is matched. Can only be used when the evaluator has a single unique field to look for
/// </summary>
public bool ForceIfFound { get; set; }
/// <summary>
/// The fields a message needs to contain for this definition
/// </summary>
public MessageFieldReference[] Fields { get; set; } = [];
/// <summary>
/// The callback for getting the identifier string
/// </summary>
public Func<SearchResult, string>? TypeIdentifierCallback { get; set; }
/// <summary>
/// The static identifier string to return when this evaluator is matched
/// </summary>
public string? StaticIdentifier { get; set; }
internal string? GetMessageType(SearchResult result)
{
if (StaticIdentifier != null)
return StaticIdentifier;
return TypeIdentifierCallback!(result);
}
internal bool Satisfied(SearchResult result)
{
foreach(var field in Fields)
{
if (!result.Contains(field))
return false;
}
return true;
}
}
}
@@ -1,15 +0,0 @@
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
internal class MessageEvalutorFieldReference
{
public bool SkipReading { get; set; }
public bool OverlappingField { get; set; }
public MessageFieldReference Field { get; set; }
public MessageTypeDefinition? ForceEvaluator { get; set; }
public MessageEvalutorFieldReference(MessageFieldReference field)
{
Field = field;
}
}
}
@@ -1,152 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// Reference to a message field
/// </summary>
public abstract class MessageFieldReference
{
/// <summary>
/// The name for this search field
/// </summary>
public string SearchName { get; set; }
/// <summary>
/// The depth at which to look for this field
/// </summary>
public int Depth { get; set; } = 1;
/// <summary>
/// Callback to check if the field value matches an expected constraint
/// </summary>
public Func<string?, bool>? Constraint { get; private set; }
/// <summary>
/// Check whether the value is one of the string values in the set
/// </summary>
public MessageFieldReference WithFilterConstraint(HashSet<string?> set)
{
Constraint = set.Contains;
return this;
}
/// <summary>
/// Check whether the value is equal to a string
/// </summary>
public MessageFieldReference WithEqualConstraint(string compare)
{
Constraint = x => x != null && x.Equals(compare, StringComparison.Ordinal);
return this;
}
/// <summary>
/// Check whether the value is not equal to a string
/// </summary>
public MessageFieldReference WithNotEqualConstraint(string compare)
{
Constraint = x => x == null || !x.Equals(compare, StringComparison.Ordinal);
return this;
}
/// <summary>
/// Check whether the value is not null
/// </summary>
public MessageFieldReference WithNotNullConstraint()
{
Constraint = x => x != null;
return this;
}
/// <summary>
/// Check whether the value starts with a certain string
/// </summary>
public MessageFieldReference WithStartsWithConstraint(string start)
{
Constraint = x => x != null && x.StartsWith(start, StringComparison.Ordinal);
return this;
}
/// <summary>
/// Check whether the value starts with a certain string
/// </summary>
public MessageFieldReference WithStartsWithConstraints(params string[] startValues)
{
Constraint = x =>
{
if (x == null)
return false;
foreach (var item in startValues)
{
if (x!.StartsWith(item, StringComparison.Ordinal))
return true;
}
return false;
};
return this;
}
/// <summary>
/// Check whether the value starts with a certain string
/// </summary>
public MessageFieldReference WithCustomConstraint(Func<string?, bool> constraint)
{
Constraint = constraint;
return this;
}
/// <summary>
/// ctor
/// </summary>
public MessageFieldReference(string searchName)
{
SearchName = searchName;
}
}
/// <summary>
/// Reference to a property message field
/// </summary>
public class PropertyFieldReference : MessageFieldReference
{
/// <summary>
/// The property name in the JSON
/// </summary>
public byte[] PropertyName { get; set; }
/// <summary>
/// Whether the property value is array values
/// </summary>
public bool ArrayValues { get; set; }
/// <summary>
/// ctor
/// </summary>
public PropertyFieldReference(string propertyName) : base(propertyName)
{
PropertyName = Encoding.UTF8.GetBytes(propertyName);
}
}
/// <summary>
/// Reference to an array message field
/// </summary>
public class ArrayFieldReference : MessageFieldReference
{
/// <summary>
/// The index in the array
/// </summary>
public int ArrayIndex { get; set; }
/// <summary>
/// ctor
/// </summary>
public ArrayFieldReference(string searchName, int depth, int index) : base(searchName)
{
Depth = depth;
ArrayIndex = index;
}
}
}
@@ -1,60 +0,0 @@
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// The results of a search for fields in a JSON message
/// </summary>
public class SearchResult
{
private List<SearchResultItem> _items = new List<SearchResultItem>();
/// <summary>
/// Get the value of a field
/// </summary>
public string? FieldValue(string searchName)
{
foreach (var item in _items)
{
if (item.Field.SearchName.Equals(searchName, StringComparison.Ordinal))
return item.Value;
}
throw new Exception($"No field value found for {searchName}");
}
/// <summary>
/// The number of found search field values
/// </summary>
public int Count => _items.Count;
/// <summary>
/// Clear the search result
/// </summary>
public void Clear() => _items.Clear();
/// <summary>
/// Whether the value for a specific field was found
/// </summary>
public bool Contains(MessageFieldReference field)
{
foreach (var item in _items)
{
if (item.Field == field)
return true;
}
return false;
}
/// <summary>
/// Write a value to the result
/// </summary>
public void Write(MessageFieldReference field, string? value) => _items.Add(new SearchResultItem
{
Field = field,
Value = value
});
}
}
@@ -1,17 +0,0 @@
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// Search result value
/// </summary>
public struct SearchResultItem
{
/// <summary>
/// The field the values is for
/// </summary>
public MessageFieldReference Field { get; set; }
/// <summary>
/// The value of the field
/// </summary>
public string? Value { get; set; }
}
}
@@ -1,49 +1,48 @@
namespace CryptoExchange.Net.Converters.MessageParsing namespace CryptoExchange.Net.Converters.MessageParsing;
/// <summary>
/// Node accessor
/// </summary>
public readonly struct NodeAccessor
{ {
/// <summary> /// <summary>
/// Node accessor /// Index
/// </summary> /// </summary>
public readonly struct NodeAccessor public int? Index { get; }
/// <summary>
/// Property name
/// </summary>
public string? Property { get; }
/// <summary>
/// Type (0 = int, 1 = string, 2 = prop name)
/// </summary>
public int Type { get; }
private NodeAccessor(int? index, string? property, int type)
{ {
/// <summary> Index = index;
/// Index Property = property;
/// </summary> Type = type;
public int? Index { get; }
/// <summary>
/// Property name
/// </summary>
public string? Property { get; }
/// <summary>
/// Type (0 = int, 1 = string, 2 = prop name)
/// </summary>
public int Type { get; }
private NodeAccessor(int? index, string? property, int type)
{
Index = index;
Property = property;
Type = type;
}
/// <summary>
/// Create an int node accessor
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static NodeAccessor Int(int value) { return new NodeAccessor(value, null, 0); }
/// <summary>
/// Create a string node accessor
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static NodeAccessor String(string value) { return new NodeAccessor(null, value, 1); }
/// <summary>
/// Create a property name node accessor
/// </summary>
/// <returns></returns>
public static NodeAccessor PropertyName() { return new NodeAccessor(null, null, 2); }
} }
/// <summary>
/// Create an int node accessor
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static NodeAccessor Int(int value) { return new NodeAccessor(value, null, 0); }
/// <summary>
/// Create a string node accessor
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static NodeAccessor String(string value) { return new NodeAccessor(null, value, 1); }
/// <summary>
/// Create a property name node accessor
/// </summary>
/// <returns></returns>
public static NodeAccessor PropertyName() { return new NodeAccessor(null, null, 2); }
} }
@@ -1,50 +1,49 @@
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
namespace CryptoExchange.Net.Converters.MessageParsing namespace CryptoExchange.Net.Converters.MessageParsing;
/// <summary>
/// Message access definition
/// </summary>
public readonly struct MessagePath : IEnumerable<NodeAccessor>
{ {
/// <summary> private readonly List<NodeAccessor> _path;
/// Message access definition
/// </summary> internal void Add(NodeAccessor node)
public readonly struct MessagePath : IEnumerable<NodeAccessor>
{ {
private readonly List<NodeAccessor> _path; _path.Add(node);
}
internal void Add(NodeAccessor node) /// <summary>
{ /// ctor
_path.Add(node); /// </summary>
} public MessagePath()
{
_path = new List<NodeAccessor>();
}
/// <summary> /// <summary>
/// ctor /// Create a new message path
/// </summary> /// </summary>
public MessagePath() /// <returns></returns>
{ public static MessagePath Get()
_path = new List<NodeAccessor>(); {
} return new MessagePath();
}
/// <summary> /// <summary>
/// Create a new message path /// IEnumerable implementation
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public static MessagePath Get() public IEnumerator<NodeAccessor> GetEnumerator()
{ {
return new MessagePath(); for (var i = 0; i < _path.Count; i++)
} yield return _path[i];
}
/// <summary> IEnumerator IEnumerable.GetEnumerator()
/// IEnumerable implementation {
/// </summary> return GetEnumerator();
/// <returns></returns>
public IEnumerator<NodeAccessor> GetEnumerator()
{
for (var i = 0; i < _path.Count; i++)
yield return _path[i];
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
} }
} }
@@ -1,43 +1,42 @@
namespace CryptoExchange.Net.Converters.MessageParsing namespace CryptoExchange.Net.Converters.MessageParsing;
/// <summary>
/// Message path extension methods
/// </summary>
public static class MessagePathExtension
{ {
/// <summary> /// <summary>
/// Message path extension methods /// Add a string node accessor
/// </summary> /// </summary>
public static class MessagePathExtension /// <param name="path"></param>
/// <param name="propName"></param>
/// <returns></returns>
public static MessagePath Property(this MessagePath path, string propName)
{ {
/// <summary> path.Add(NodeAccessor.String(propName));
/// Add a string node accessor return path;
/// </summary> }
/// <param name="path"></param>
/// <param name="propName"></param>
/// <returns></returns>
public static MessagePath Property(this MessagePath path, string propName)
{
path.Add(NodeAccessor.String(propName));
return path;
}
/// <summary> /// <summary>
/// Add a property name node accessor /// Add a property name node accessor
/// </summary> /// </summary>
/// <param name="path"></param> /// <param name="path"></param>
/// <returns></returns> /// <returns></returns>
public static MessagePath PropertyName(this MessagePath path) public static MessagePath PropertyName(this MessagePath path)
{ {
path.Add(NodeAccessor.PropertyName()); path.Add(NodeAccessor.PropertyName());
return path; return path;
} }
/// <summary> /// <summary>
/// Add a int node accessor /// Add a int node accessor
/// </summary> /// </summary>
/// <param name="path"></param> /// <param name="path"></param>
/// <param name="index"></param> /// <param name="index"></param>
/// <returns></returns> /// <returns></returns>
public static MessagePath Index(this MessagePath path, int index) public static MessagePath Index(this MessagePath path, int index)
{ {
path.Add(NodeAccessor.Int(index)); path.Add(NodeAccessor.Int(index));
return path; return path;
}
} }
} }
@@ -1,21 +1,20 @@
namespace CryptoExchange.Net.Converters.MessageParsing namespace CryptoExchange.Net.Converters.MessageParsing;
/// <summary>
/// Message node type
/// </summary>
public enum NodeType
{ {
/// <summary> /// <summary>
/// Message node type /// Array node
/// </summary> /// </summary>
public enum NodeType Array,
{ /// <summary>
/// <summary> /// Object node
/// Array node /// </summary>
/// </summary> Object,
Array, /// <summary>
/// <summary> /// Value node
/// Object node /// </summary>
/// </summary> Value
Object,
/// <summary>
/// Value node
/// </summary>
Value
}
} }
@@ -1,246 +1,232 @@
using CryptoExchange.Net.Exceptions;
using System; using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Text.Json;
using System.Collections.Generic;
#if NET5_0_OR_GREATER
using System.Diagnostics.CodeAnalysis;
#endif
using System.Threading; using System.Threading;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson;
{
/// <summary>
/// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties
/// with [ArrayProperty(x)] where x is the index of the property in the array
/// </summary>
#if NET5_0_OR_GREATER
public class ArrayConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : JsonConverter<T> where T : new()
#else
public class ArrayConverter<T> : JsonConverter<T> where T : new()
#endif
{
private static SortedDictionary<int, List<ArrayPropertyInfo>>? _typePropertyInfo;
/// <inheritdoc /> /// <summary>
/// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties
/// with [ArrayProperty(x)] where x is the index of the property in the array
/// </summary>
#if NET5_0_OR_GREATER #if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] public class ArrayConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : JsonConverter<T> where T : new()
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] #else
public class ArrayConverter<T> : JsonConverter<T> where T : new()
#endif #endif
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) {
private static readonly Lazy<List<ArrayPropertyInfo>> _typePropertyInfo = new Lazy<List<ArrayPropertyInfo>>(CacheTypeAttributes, LazyThreadSafetyMode.PublicationOnly);
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
if (value == null)
{ {
if (value == null) writer.WriteNullValue();
return;
}
writer.WriteStartArray();
var ordered = _typePropertyInfo.Value.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index);
var last = -1;
foreach (var prop in ordered)
{
if (prop.ArrayProperty.Index == last)
continue;
while (prop.ArrayProperty.Index != last + 1)
{ {
writer.WriteNullValue(); writer.WriteNullValue();
return; last += 1;
} }
if (_typePropertyInfo == null) last = prop.ArrayProperty.Index;
_typePropertyInfo = CacheTypeAttributes();
writer.WriteStartArray(); var objValue = prop.PropertyInfo.GetValue(value);
var last = -1; if (objValue == null)
foreach (var indexProps in _typePropertyInfo)
{ {
foreach (var prop in indexProps.Value) writer.WriteNullValue();
continue;
}
JsonSerializerOptions? typeOptions = null;
if (prop.JsonConverter != null)
{
typeOptions = new JsonSerializerOptions
{ {
if (prop.ArrayProperty.Index == last) NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
// Don't write the same index twice PropertyNameCaseInsensitive = false,
continue; TypeInfoResolver = options.TypeInfoResolver,
};
typeOptions.Converters.Add(prop.JsonConverter);
}
while (prop.ArrayProperty.Index != last + 1) if (prop.JsonConverter == null && IsSimple(prop.PropertyInfo.PropertyType))
{
if (prop.TargetType == typeof(string))
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
else if (prop.TargetType == typeof(bool))
writer.WriteBooleanValue((bool)objValue);
else
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!);
}
else
{
JsonSerializer.Serialize(writer, objValue, typeOptions ?? options);
}
}
writer.WriteEndArray();
}
/// <inheritdoc />
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return default;
var result = new T();
return ParseObject(ref reader, result, options);
}
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
#else
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
#endif
{
if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception("Not an array");
int index = 0;
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
break;
var indexAttributes = _typePropertyInfo.Value.Where(a => a.ArrayProperty.Index == index);
if (!indexAttributes.Any())
{
index++;
continue;
}
foreach (var attribute in indexAttributes)
{
var targetType = attribute.TargetType;
object? value = null;
if (attribute.JsonConverter != null)
{
if (attribute.JsonSerializerOptions == null)
{ {
writer.WriteNullValue(); attribute.JsonSerializerOptions = new JsonSerializerOptions
last += 1;
}
last = prop.ArrayProperty.Index;
var objValue = prop.PropertyInfo.GetValue(value);
if (objValue == null)
{
writer.WriteNullValue();
continue;
}
JsonSerializerOptions? typeOptions = null;
if (prop.JsonConverter != null)
{
typeOptions = new JsonSerializerOptions
{ {
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals, NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
PropertyNameCaseInsensitive = false, PropertyNameCaseInsensitive = false,
Converters = { attribute.JsonConverter },
TypeInfoResolver = options.TypeInfoResolver, TypeInfoResolver = options.TypeInfoResolver,
}; };
typeOptions.Converters.Add(prop.JsonConverter);
} }
if (prop.JsonConverter == null && IsSimple(prop.PropertyInfo.PropertyType)) var doc = JsonDocument.ParseValue(ref reader);
{ value = doc.Deserialize(attribute.PropertyInfo.PropertyType, attribute.JsonSerializerOptions);
if (prop.TargetType == typeof(string))
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
else if (prop.TargetType == typeof(bool))
writer.WriteBooleanValue((bool)objValue);
else
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!);
}
else
{
JsonSerializer.Serialize(writer, objValue, typeOptions ?? options);
}
} }
else if (attribute.DefaultDeserialization)
{
value = JsonDocument.ParseValue(ref reader).Deserialize(options.GetTypeInfo(attribute.PropertyInfo.PropertyType));
}
else
{
value = reader.TokenType switch
{
JsonTokenType.Null => null,
JsonTokenType.False => false,
JsonTokenType.True => true,
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetDecimal(),
JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options),
_ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"),
};
}
if (targetType.IsAssignableFrom(value?.GetType()))
attribute.PropertyInfo.SetValue(result, value);
else
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
} }
writer.WriteEndArray(); index++;
} }
/// <inheritdoc /> return result;
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) }
private static bool IsSimple(Type type)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{ {
if (reader.TokenType == JsonTokenType.Null) // nullable type, check if the nested type is simple.
return default; return IsSimple(type.GetGenericArguments()[0]);
var result = new T();
return ParseObject(ref reader, result, options);
} }
return type.IsPrimitive
|| type.IsEnum
|| type == typeof(string)
|| type == typeof(decimal);
}
#if NET5_0_OR_GREATER #if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options) private static List<ArrayPropertyInfo> CacheTypeAttributes()
#else #else
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options) private static List<ArrayPropertyInfo> CacheTypeAttributes()
#endif #endif
{
var attributes = new List<ArrayPropertyInfo>();
var properties = typeof(T).GetProperties();
foreach (var property in properties)
{ {
if (reader.TokenType != JsonTokenType.StartArray) var att = property.GetCustomAttribute<ArrayPropertyAttribute>();
throw new CeDeserializationException("Not an array"); if (att == null)
continue;
var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
if (_typePropertyInfo == null) var converterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? targetType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType;
_typePropertyInfo = CacheTypeAttributes(); attributes.Add(new ArrayPropertyInfo
int index = 0;
while (reader.Read())
{ {
if (reader.TokenType == JsonTokenType.EndArray) ArrayProperty = att,
break; PropertyInfo = property,
DefaultDeserialization = property.GetCustomAttribute<CryptoExchange.Net.Attributes.JsonConversionAttribute>() != null,
if(!_typePropertyInfo.TryGetValue(index, out var indexAttributes)) JsonConverter = converterType == null ? null : (JsonConverter)Activator.CreateInstance(converterType)!,
{ TargetType = targetType
index++; });
continue;
}
foreach (var attribute in indexAttributes)
{
var targetType = attribute.TargetType;
object? value = null;
if (attribute.JsonConverter != null)
{
if (attribute.JsonSerializerOptions == null)
{
attribute.JsonSerializerOptions = new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
PropertyNameCaseInsensitive = false,
Converters = { attribute.JsonConverter },
TypeInfoResolver = options.TypeInfoResolver,
};
}
var doc = JsonDocument.ParseValue(ref reader);
value = doc.Deserialize(attribute.PropertyInfo.PropertyType, attribute.JsonSerializerOptions);
}
else if (attribute.DefaultDeserialization)
{
value = JsonDocument.ParseValue(ref reader).Deserialize(options.GetTypeInfo(attribute.PropertyInfo.PropertyType));
}
else
{
value = reader.TokenType switch
{
JsonTokenType.Null => null,
JsonTokenType.False => false,
JsonTokenType.True => true,
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetDecimal(),
JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options),
_ => throw new CeDeserializationException($"Array deserialization of type {reader.TokenType} not supported"),
};
}
if (targetType.IsAssignableFrom(value?.GetType()))
attribute.PropertyInfo.SetValue(result, value);
else
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
}
index++;
}
return result;
} }
private static bool IsSimple(Type type) return attributes;
{ }
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 private class ArrayPropertyInfo
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] {
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] public PropertyInfo PropertyInfo { get; set; } = null!;
private static SortedDictionary<int, List<ArrayPropertyInfo>> CacheTypeAttributes() public ArrayPropertyAttribute ArrayProperty { get; set; } = null!;
#else public JsonConverter? JsonConverter { get; set; }
private static SortedDictionary<int, List<ArrayPropertyInfo>> CacheTypeAttributes() public bool DefaultDeserialization { get; set; }
#endif public Type TargetType { get; set; } = null!;
{ public JsonSerializerOptions? JsonSerializerOptions { get; set; }
var result = new SortedDictionary<int, List<ArrayPropertyInfo>>();
var properties = typeof(T).GetProperties();
foreach (var property in properties)
{
var att = property.GetCustomAttribute<ArrayPropertyAttribute>();
if (att == null)
continue;
var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
var converterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? targetType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType;
if (!result.TryGetValue(att.Index, out var indexList))
{
indexList = new List<ArrayPropertyInfo>();
result[att.Index] = indexList;
}
indexList.Add(new ArrayPropertyInfo
{
ArrayProperty = att,
PropertyInfo = property,
DefaultDeserialization = property.GetCustomAttribute<CryptoExchange.Net.Attributes.JsonConversionAttribute>() != null,
JsonConverter = converterType == null ? null : (JsonConverter)Activator.CreateInstance(converterType)!,
TargetType = targetType
});
}
return result;
}
private class ArrayPropertyInfo
{
public PropertyInfo PropertyInfo { get; set; } = null!;
public ArrayPropertyAttribute ArrayProperty { get; set; } = null!;
public JsonConverter? JsonConverter { get; set; }
public bool DefaultDeserialization { get; set; }
public Type TargetType { get; set; } = null!;
public JsonSerializerOptions? JsonSerializerOptions { get; set; } = null;
}
} }
} }
@@ -1,46 +1,45 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson;
{
/// <summary>
/// Decimal converter that handles overflowing decimal values (by setting it to decimal.MaxValue)
/// </summary>
public class BigDecimalConverter : JsonConverter<decimal>
{
/// <inheritdoc />
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
try
{
return decimal.Parse(reader.GetString()!, NumberStyles.Float, CultureInfo.InvariantCulture);
}
catch(OverflowException)
{
// Value doesn't fit decimal, default to max value
return decimal.MaxValue;
}
}
/// <summary>
/// Decimal converter that handles overflowing decimal values (by setting it to decimal.MaxValue)
/// </summary>
public class BigDecimalConverter : JsonConverter<decimal>
{
/// <inheritdoc />
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
try try
{ {
return reader.GetDecimal(); return decimal.Parse(reader.GetString()!, NumberStyles.Float, CultureInfo.InvariantCulture);
} }
catch(FormatException) catch(OverflowException)
{ {
// Format issue, assume value is too large // Value doesn't fit decimal, default to max value
return decimal.MaxValue; return decimal.MaxValue;
} }
} }
/// <inheritdoc /> try
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
{ {
writer.WriteNumberValue(value); return reader.GetDecimal();
}
catch(FormatException)
{
// Format issue, assume value is too large
return decimal.MaxValue;
} }
} }
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
{
writer.WriteNumberValue(value);
}
} }
@@ -1,54 +1,34 @@
using Microsoft.Extensions.Logging;
using System; using System;
using System.Diagnostics;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson;
/// <summary>
/// Bool converter
/// </summary>
public class BoolConverter : JsonConverterFactory
{ {
/// <summary> /// <inheritdoc />
/// Bool converter public override bool CanConvert(Type typeToConvert)
/// </summary>
public class BoolConverter : JsonConverterFactory
{ {
/// <inheritdoc /> return typeToConvert == typeof(bool) || typeToConvert == typeof(bool?);
public override bool CanConvert(Type typeToConvert) }
{
return typeToConvert == typeof(bool) || typeToConvert == typeof(bool?);
}
/// <inheritdoc /> /// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{ {
return typeToConvert == typeof(bool) ? new BoolConverterInner() : new BoolConverterInnerNullable(); return typeToConvert == typeof(bool) ? new BoolConverterInner<bool>() : new BoolConverterInner<bool?>();
} }
private class BoolConverterInnerNullable : JsonConverter<bool?> private class BoolConverterInner<T> : JsonConverter<T>
{ {
public override bool? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> ReadBool(ref reader, typeToConvert, options); => (T)((object?)ReadBool(ref reader, typeToConvert, options) ?? default(T))!;
public override void Write(Utf8JsonWriter writer, bool? value, JsonSerializerOptions options) public static bool? ReadBool(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (value is bool boolVal)
writer.WriteBooleanValue(boolVal);
else
writer.WriteNullValue();
}
}
private class BoolConverterInner : JsonConverter<bool>
{
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> ReadBool(ref reader, typeToConvert, options) ?? false;
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
{
writer.WriteBooleanValue(value);
}
}
private static bool? ReadBool(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
if (reader.TokenType == JsonTokenType.True) if (reader.TokenType == JsonTokenType.True)
return true; return true;
@@ -67,7 +47,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value))
{ {
if (typeToConvert == typeof(bool)) if (typeToConvert == typeof(bool))
LibraryHelpers.StaticLogger?.LogWarning("Received null or empty bool value, but property type is not a nullable bool. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name); Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null bool value, but property type is not a nullable bool");
return default; return default;
} }
@@ -91,5 +71,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
throw new SerializationException($"Can't convert bool value {value}"); throw new SerializationException($"Can't convert bool value {value}");
} }
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
if (value is bool boolVal)
writer.WriteBooleanValue(boolVal);
else
writer.WriteNullValue();
}
} }
} }
@@ -1,35 +1,36 @@
using System; using System;
#if NET5_0_OR_GREATER
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
#endif
using System.Linq; using System.Linq;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson;
{
/// <summary> /// <summary>
/// Converter for comma separated enum values /// Converter for comma separated enum values
/// </summary> /// </summary>
#if NET5_0_OR_GREATER #if NET5_0_OR_GREATER
public class CommaSplitEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T> : JsonConverter<T[]> where T : struct, Enum public class CommaSplitEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T> : JsonConverter<T[]> where T : struct, Enum
#else #else
public class CommaSplitEnumConverter<T> : JsonConverter<T[]> where T : struct, Enum public class CommaSplitEnumConverter<T> : JsonConverter<T[]> where T : struct, Enum
#endif #endif
{
/// <inheritdoc />
public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
/// <inheritdoc /> var str = reader.GetString();
public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) if (string.IsNullOrEmpty(str))
{ return [];
var str = reader.GetString();
if (string.IsNullOrEmpty(str))
return [];
return str!.Split(',').Select(x => (T)EnumConverter.ParseString<T>(x)!).ToArray() ?? []; return str!.Split(',').Select(x => (T)EnumConverter.ParseString<T>(x)!).ToArray() ?? [];
} }
/// <inheritdoc /> /// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options) public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options)
{ {
writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x)))); writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x))));
}
} }
} }
@@ -1,30 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter for comma separated string values
/// </summary>
public class CommaSplitStringConverter : JsonConverter<string[]>
{
/// <inheritdoc />
public override string[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var str = reader.GetString();
if (string.IsNullOrEmpty(str))
return [];
return str!.Split(',').ToArray() ?? [];
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, string[] value, JsonSerializerOptions options)
{
writer.WriteStringValue(string.Join(",", value));
}
}
}
@@ -1,98 +1,68 @@
using Microsoft.Extensions.Logging;
using System; using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson;
/// <summary>
/// Date time converter
/// </summary>
public class DateTimeConverter : JsonConverterFactory
{ {
/// <summary> private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
/// Date time converter private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
/// </summary> private const double _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000d;
public class DateTimeConverter : JsonConverterFactory private const double _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000d / 1000;
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{ {
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); return typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTime?);
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000; }
private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000m;
private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
/// <inheritdoc /> /// <inheritdoc />
public override bool CanConvert(Type typeToConvert) public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{ {
return typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTime?); return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner<DateTime>() : new DateTimeConverterInner<DateTime?>();
} }
/// <inheritdoc /> private class DateTimeConverterInner<T> : JsonConverter<T>
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) {
{ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner() : new NullableDateTimeConverterInner(); => (T)((object?)ReadDateTime(ref reader, typeToConvert, options) ?? default(T))!;
}
private class NullableDateTimeConverterInner : JsonConverter<DateTime?>
{
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> ReadDateTime(ref reader, typeToConvert, options);
public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
{
if (value == null)
{
writer.WriteNullValue();
return;
}
if (value.Value == default)
writer.WriteStringValue(default(DateTime));
else
writer.WriteNumberValue((long)Math.Round((value.Value - new DateTime(1970, 1, 1)).TotalMilliseconds));
}
}
private class DateTimeConverterInner : JsonConverter<DateTime>
{
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> ReadDateTime(ref reader, typeToConvert, options) ?? default;
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
var dtValue = value;
if (dtValue == default)
writer.WriteStringValue(default(DateTime));
else
writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds));
}
}
private static DateTime? ReadDateTime(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) private static DateTime? ReadDateTime(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
if (reader.TokenType == JsonTokenType.Null) if (reader.TokenType == JsonTokenType.Null)
{ {
if (typeToConvert == typeof(DateTime)) if (typeToConvert == typeof(DateTime))
LibraryHelpers.StaticLogger?.LogWarning("DateTime value of null, but property is not nullable. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name); Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | DateTime value of null, but property is not nullable");
return default; return default;
} }
if (reader.TokenType is JsonTokenType.Number) if (reader.TokenType is JsonTokenType.Number)
{ {
var decValue = reader.GetDecimal(); var longValue = reader.GetDouble();
if (decValue == 0 || decValue < 0) if (longValue == 0 || longValue < 0)
return default; return default;
return ParseFromDecimal(decValue); return ParseFromDouble(longValue);
} }
else if (reader.TokenType is JsonTokenType.String) else if (reader.TokenType is JsonTokenType.String)
{ {
var stringValue = reader.GetString(); var stringValue = reader.GetString();
if (string.IsNullOrWhiteSpace(stringValue) if (string.IsNullOrWhiteSpace(stringValue)
|| stringValue!.Equals("-1", StringComparison.Ordinal) || stringValue == "-1"
|| stringValue!.Equals("0001-01-01T00:00:00Z", StringComparison.OrdinalIgnoreCase) || stringValue == "0001-01-01T00:00:00Z"
|| decimal.TryParse(stringValue, out var decVal) && decVal == 0) || double.TryParse(stringValue, out var doubleVal) && doubleVal == 0)
{ {
return default; return default;
} }
return ParseFromString(stringValue!, options.TypeInfoResolver?.GetType()?.Name); return ParseFromString(stringValue!);
} }
else else
{ {
@@ -100,174 +70,172 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
} }
} }
/// <summary> public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
/// Parse a double value to datetime
/// </summary>
public static DateTime ParseFromDouble(double value)
=> ParseFromDecimal((decimal)value);
/// <summary>
/// Parse a decimal value to datetime
/// </summary>
public static DateTime ParseFromDecimal(decimal value)
{ {
if (value < 19999999999) if (value == null)
return ConvertFromSeconds(value); {
if (value < 19999999999999) writer.WriteNullValue();
return ConvertFromMilliseconds(value); }
if (value < 19999999999999999) else
return ConvertFromMicroseconds(value); {
var dtValue = (DateTime)(object)value;
return ConvertFromNanoseconds(value); if (dtValue == default)
writer.WriteStringValue(default(DateTime));
else
writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds));
}
} }
/// <summary>
/// Parse a string value to datetime
/// </summary>
public static DateTime ParseFromString(string stringValue, string? resolverName)
{
if (stringValue!.Length == 12 && stringValue.StartsWith("202", StringComparison.OrdinalIgnoreCase))
{
// Parse 202303261200 format
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
{
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
return default;
}
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
}
if (stringValue.Length == 8)
{
// Parse 20211103 format
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
{
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
return default;
}
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
}
if (stringValue.Length == 6)
{
// Parse 211103 format
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
{
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
return default;
}
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
}
if (decimal.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var decimalValue))
{
// Parse 1637745563.000 format
if (decimalValue <= 0)
return default;
if (decimalValue < 19999999999)
return ConvertFromSeconds(decimalValue);
if (decimalValue < 19999999999999)
return ConvertFromMilliseconds(decimalValue);
if (decimalValue < 19999999999999999)
return ConvertFromMicroseconds(decimalValue);
return ConvertFromNanoseconds(decimalValue);
}
if (stringValue.Length == 10)
{
// Parse 2021-11-03 format
var values = stringValue.Split('-');
if (!int.TryParse(values[0], out var year)
|| !int.TryParse(values[1], out var month)
|| !int.TryParse(values[2], out var day))
{
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
return default;
}
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
}
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
}
/// <summary>
/// Convert a seconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromSeconds(decimal seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromSeconds(double seconds) => ConvertFromSeconds((decimal)seconds);
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromSeconds(long seconds) => ConvertFromSeconds((decimal)seconds);
/// <summary>
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromMilliseconds(decimal milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromMilliseconds(double milliseconds) => ConvertFromMilliseconds((decimal)milliseconds);
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromMilliseconds(long milliseconds) => ConvertFromMilliseconds((decimal)milliseconds);
/// <summary>
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromMicroseconds(decimal microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromMicroseconds(double microseconds) => ConvertFromMicroseconds((decimal)microseconds);
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromMicroseconds(long microseconds) => ConvertFromMicroseconds((decimal)microseconds);
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromNanoseconds(decimal nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromNanoseconds(double nanoseconds) => ConvertFromNanoseconds((decimal)nanoseconds);
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromNanoseconds(long nanoseconds) => ConvertFromNanoseconds((decimal)nanoseconds);
/// <summary>
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
/// </summary>
[return: NotNullIfNotNull("time")]
public static long? ConvertToSeconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalSeconds);
/// <summary>
/// Convert a DateTime value to milliseconds since epoch (01-01-1970) value
/// </summary>
[return: NotNullIfNotNull("time")]
public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds);
/// <summary>
/// Convert a DateTime value to microseconds since epoch (01-01-1970) value
/// </summary>
[return: NotNullIfNotNull("time")]
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond);
/// <summary>
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
/// </summary>
[return: NotNullIfNotNull("time")]
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
} }
/// <summary>
/// Parse a long value to datetime
/// </summary>
/// <param name="longValue"></param>
/// <returns></returns>
public static DateTime ParseFromDouble(double longValue)
{
if (longValue < 19999999999)
return ConvertFromSeconds(longValue);
if (longValue < 19999999999999)
return ConvertFromMilliseconds(longValue);
if (longValue < 19999999999999999)
return ConvertFromMicroseconds(longValue);
return ConvertFromNanoseconds(longValue);
}
/// <summary>
/// Parse a string value to datetime
/// </summary>
/// <param name="stringValue"></param>
/// <returns></returns>
public static DateTime ParseFromString(string stringValue)
{
if (stringValue!.Length == 12 && stringValue.StartsWith("202"))
{
// Parse 202303261200 format
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
{
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
return default;
}
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
}
if (stringValue.Length == 8)
{
// Parse 20211103 format
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
{
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
return default;
}
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
}
if (stringValue.Length == 6)
{
// Parse 211103 format
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
{
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
return default;
}
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
}
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
{
// Parse 1637745563.000 format
if (doubleValue <= 0)
return default;
if (doubleValue < 19999999999)
return ConvertFromSeconds(doubleValue);
if (doubleValue < 19999999999999)
return ConvertFromMilliseconds((long)doubleValue);
if (doubleValue < 19999999999999999)
return ConvertFromMicroseconds((long)doubleValue);
return ConvertFromNanoseconds((long)doubleValue);
}
if (stringValue.Length == 10)
{
// Parse 2021-11-03 format
var values = stringValue.Split('-');
if (!int.TryParse(values[0], out var year)
|| !int.TryParse(values[1], out var month)
|| !int.TryParse(values[2], out var day))
{
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
return default;
}
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
}
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
}
/// <summary>
/// Convert a seconds since epoch (01-01-1970) value to DateTime
/// </summary>
/// <param name="seconds"></param>
/// <returns></returns>
public static DateTime ConvertFromSeconds(double seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
/// <summary>
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
/// </summary>
/// <param name="milliseconds"></param>
/// <returns></returns>
public static DateTime ConvertFromMilliseconds(double milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
/// <summary>
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
/// </summary>
/// <param name="microseconds"></param>
/// <returns></returns>
public static DateTime ConvertFromMicroseconds(double microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
/// <param name="nanoseconds"></param>
/// <returns></returns>
public static DateTime ConvertFromNanoseconds(double nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
/// <summary>
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")]
public static long? ConvertToSeconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalSeconds);
/// <summary>
/// Convert a DateTime value to milliseconds since epoch (01-01-1970) value
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")]
public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds);
/// <summary>
/// Convert a DateTime value to microseconds since epoch (01-01-1970) value
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")]
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond);
/// <summary>
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")]
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
} }
@@ -1,44 +1,43 @@
using System; using System;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson;
/// <summary>
/// Decimal converter
/// </summary>
public class DecimalConverter : JsonConverter<decimal?>
{ {
/// <summary> /// <inheritdoc />
/// Decimal converter public override decimal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
/// </summary>
public class DecimalConverter : JsonConverter<decimal?>
{ {
/// <inheritdoc /> if (reader.TokenType == JsonTokenType.Null)
public override decimal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) return null;
if (reader.TokenType == JsonTokenType.String)
{ {
if (reader.TokenType == JsonTokenType.Null) var value = reader.GetString();
return null; return ExchangeHelpers.ParseDecimal(value);
if (reader.TokenType == JsonTokenType.String)
{
var value = reader.GetString();
return ExchangeHelpers.ParseDecimal(value);
}
try
{
return reader.GetDecimal();
}
catch(FormatException)
{
// Format issue, assume value is too large
return decimal.MaxValue;
}
} }
/// <inheritdoc /> try
public override void Write(Utf8JsonWriter writer, decimal? value, JsonSerializerOptions options)
{ {
if (value == null) return reader.GetDecimal();
writer.WriteNullValue(); }
else catch(FormatException)
writer.WriteNumberValue(value.Value); {
// Format issue, assume value is too large
return decimal.MaxValue;
} }
} }
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, decimal? value, JsonSerializerOptions options)
{
if (value == null)
writer.WriteNullValue();
else
writer.WriteNumberValue(value.Value);
}
} }
@@ -1,23 +1,22 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson;
{
/// <summary>
/// Converter for serializing decimal values as string
/// </summary>
public class DecimalStringWriterConverter : JsonConverter<decimal>
{
/// <inheritdoc />
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
/// <inheritdoc /> /// <summary>
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options) /// Converter for serializing decimal values as string
=> writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture) ?? null); /// </summary>
public class DecimalStringWriterConverter : JsonConverter<decimal>
{
/// <inheritdoc />
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
} }
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture) ?? null);
} }
@@ -1,359 +1,288 @@
using CryptoExchange.Net.Attributes; using CryptoExchange.Net.Attributes;
using Microsoft.Extensions.Logging;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
#if NET8_0_OR_GREATER
using System.Collections.Frozen;
#endif
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson;
/// <summary>
/// Static EnumConverter methods
/// </summary>
public static class EnumConverter
{ {
/// <summary> /// <summary>
/// Static EnumConverter methods /// Get the enum value from a string
/// </summary> /// </summary>
public static class EnumConverter /// <param name="value">String value</param>
{ /// <returns></returns>
/// <summary>
/// Get the enum value from a string
/// </summary>
/// <param name="value">String value</param>
/// <returns></returns>
#if NET5_0_OR_GREATER #if NET5_0_OR_GREATER
public static T? ParseString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string value) where T : struct, Enum public static T? ParseString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string value) where T : struct, Enum
#else #else
public static T? ParseString<T>(string value) where T : struct, Enum public static T? ParseString<T>(string value) where T : struct, Enum
#endif #endif
=> EnumConverter<T>.ParseString(value); => EnumConverter<T>.ParseString(value);
/// <summary>
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
/// </summary>
/// <param name="enumValue"></param>
/// <returns></returns>
#if NET5_0_OR_GREATER
public static string GetString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(T enumValue) where T : struct, Enum
#else
public static string GetString<T>(T enumValue) where T : struct, Enum
#endif
=> EnumConverter<T>.GetString(enumValue);
/// <summary>
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
/// </summary>
/// <param name="enumValue"></param>
/// <returns></returns>
[return: NotNullIfNotNull("enumValue")]
#if NET5_0_OR_GREATER
public static string? GetString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(T? enumValue) where T : struct, Enum
#else
public static string? GetString<T>(T? enumValue) where T : struct, Enum
#endif
=> EnumConverter<T>.GetString(enumValue);
}
/// <summary> /// <summary>
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value /// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
/// </summary> /// </summary>
/// <param name="enumValue"></param>
/// <returns></returns>
#if NET5_0_OR_GREATER #if NET5_0_OR_GREATER
public class EnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T> public static string GetString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(T enumValue) where T : struct, Enum
#else #else
public class EnumConverter<T> public static string GetString<T>(T enumValue) where T : struct, Enum
#endif #endif
: JsonConverter<T>, INullableConverterFactory where T : struct, Enum => EnumConverter<T>.GetString(enumValue);
/// <summary>
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
/// </summary>
/// <param name="enumValue"></param>
/// <returns></returns>
[return: NotNullIfNotNull("enumValue")]
#if NET5_0_OR_GREATER
public static string? GetString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(T? enumValue) where T : struct, Enum
#else
public static string? GetString<T>(T? enumValue) where T : struct, Enum
#endif
=> EnumConverter<T>.GetString(enumValue);
}
/// <summary>
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
/// </summary>
#if NET5_0_OR_GREATER
public class EnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>
#else
public class EnumConverter<T>
#endif
: JsonConverter<T>, INullableConverterFactory where T : struct, Enum
{
private static List<KeyValuePair<T, string>>? _mapping;
private NullableEnumConverter? _nullableEnumConverter;
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
internal class NullableEnumConverter : JsonConverter<T?>
{ {
class EnumMapping private readonly EnumConverter<T> _enumConverter;
{
public T Value { get; set; }
public string StringValue { get; set; }
public EnumMapping(T value, string stringValue) public NullableEnumConverter(EnumConverter<T> enumConverter)
{ {
Value = value; _enumConverter = enumConverter;
StringValue = stringValue; }
} public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return EnumConverter<T>.ReadNullable(ref reader, typeToConvert, options, out var isEmptyString, out var warn);
} }
#if NET8_0_OR_GREATER public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
private static FrozenSet<EnumMapping>? _mappingToEnum = null;
private static FrozenDictionary<T, string>? _mappingToString = null;
#else
private static List<EnumMapping>? _mappingToEnum = null;
private static Dictionary<T, string>? _mappingToString = null;
#endif
private NullableEnumConverter? _nullableEnumConverter = null;
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
internal class NullableEnumConverter : JsonConverter<T?>
{ {
private readonly EnumConverter<T> _enumConverter; if (value == null)
public NullableEnumConverter(EnumConverter<T> enumConverter)
{ {
_enumConverter = enumConverter; writer.WriteNullValue();
}
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return _enumConverter.ReadNullable(ref reader, typeToConvert, options, out var isEmptyString);
}
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
{
if (value == null)
{
writer.WriteNullValue();
}
else
{
_enumConverter.Write(writer, value.Value, options);
}
}
}
/// <inheritdoc />
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString);
if (t == null)
{
if (isEmptyString && !_unknownValuesWarned.Contains(null))
{
// We received an empty string and have no mapping for it, and the property isn't nullable
LibraryHelpers.StaticLogger?.LogWarning($"Received null or empty enum value, but property type is not a nullable enum. EnumType: {typeof(T).FullName}. If you think {typeof(T).FullName} should be nullable please open an issue on the Github repo");
}
return new T(); // return default value
} }
else else
{ {
return t.Value; _enumConverter.Write(writer, value.Value, options);
} }
} }
}
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString) /// <inheritdoc />
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString, out var warn);
if (t == null)
{ {
isEmptyString = false; if (warn)
var enumType = typeof(T);
if (_mappingToEnum == null)
CreateMapping();
var stringValue = reader.TokenType switch
{ {
JsonTokenType.String => reader.GetString(), if (isEmptyString)
JsonTokenType.Number => reader.GetInt32().ToString(),
JsonTokenType.True => reader.GetBoolean().ToString(),
JsonTokenType.False => reader.GetBoolean().ToString(),
JsonTokenType.Null => null,
_ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType)
};
if (stringValue is null)
return null;
if (!GetValue(enumType, stringValue, out var result))
{
if (string.IsNullOrWhiteSpace(stringValue))
{ {
isEmptyString = true; // We received an empty string and have no mapping for it, and the property isn't nullable
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received empty string as enum value, but property type is not a nullable enum. EnumType: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo");
} }
else else
{ {
// We received an enum value but weren't able to parse it. Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null enum value, but property type is not a nullable enum. EnumType: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo");
if (!_unknownValuesWarned.Contains(stringValue))
{
_unknownValuesWarned.Add(stringValue!);
LibraryHelpers.StaticLogger?.LogWarning($"Cannot map enum value. EnumType: {enumType.FullName}, Value: {stringValue}, Known values: {string.Join(", ", _mappingToEnum!.Select(m => m.Value))}. If you think {stringValue} should added please open an issue on the Github repo");
}
} }
return null;
} }
return result; return new T(); // return default value
} }
else
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{ {
var stringValue = GetString(value); return t.Value;
writer.WriteStringValue(stringValue);
}
private static bool GetValue(Type objectType, string value, out T? result)
{
if (_mappingToEnum != null)
{
EnumMapping? mapping = null;
// Try match on full equals
foreach (var item in _mappingToEnum)
{
if (item.StringValue.Equals(value, StringComparison.Ordinal))
{
mapping = item;
break;
}
}
// If not found, try matching ignoring case
if (mapping == null)
{
foreach (var item in _mappingToEnum)
{
if (item.StringValue.Equals(value, StringComparison.OrdinalIgnoreCase))
{
mapping = item;
break;
}
}
}
if (mapping != null)
{
result = mapping.Value;
return true;
}
}
if (objectType.IsDefined(typeof(FlagsAttribute)))
{
var intValue = int.Parse(value);
result = (T)Enum.ToObject(objectType, intValue);
return true;
}
if (_unknownValuesWarned.Contains(value))
{
// Check if it is an known unknown value
// Done here to prevent lookup overhead for normal conversions, but prevent expensive exception throwing
result = default;
return false;
}
if (String.IsNullOrEmpty(value))
{
// An empty/null value will always fail when parsing, so just return here
result = default;
return false;
}
try
{
// If no explicit mapping is found try to parse string
result = (T)Enum.Parse(objectType, value, true);
return true;
}
catch (Exception)
{
result = default;
return false;
}
}
private static void CreateMapping()
{
var mappingToEnum = new List<EnumMapping>();
var mappingToString = new Dictionary<T, string>();
var enumType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
var enumMembers = enumType.GetFields();
foreach (var member in enumMembers)
{
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
foreach (MapAttribute attribute in maps)
{
foreach (var value in attribute.Values)
{
var enumVal = (T)Enum.Parse(enumType, member.Name);
mappingToEnum.Add(new EnumMapping(enumVal, value));
if (!mappingToString.ContainsKey(enumVal))
mappingToString.Add(enumVal, value);
}
}
}
#if NET8_0_OR_GREATER
_mappingToEnum = mappingToEnum.ToFrozenSet();
_mappingToString = mappingToString.ToFrozenDictionary();
#else
_mappingToEnum = mappingToEnum;
_mappingToString = mappingToString;
#endif
}
/// <summary>
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
/// </summary>
/// <param name="enumValue"></param>
/// <returns></returns>
[return: NotNullIfNotNull("enumValue")]
public static string? GetString(T? enumValue)
{
if (_mappingToString == null)
CreateMapping();
return enumValue == null ? null : (_mappingToString!.TryGetValue(enumValue.Value, out var str) ? str : enumValue.ToString());
}
/// <summary>
/// Get the enum value from a string
/// </summary>
/// <param name="value">String value</param>
/// <returns></returns>
public static T? ParseString(string value)
{
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
if (_mappingToEnum == null)
CreateMapping();
EnumMapping? mapping = null;
// Try match on full equals
foreach(var item in _mappingToEnum!)
{
if (item.StringValue.Equals(value, StringComparison.Ordinal))
{
mapping = item;
break;
}
}
// If not found, try matching ignoring case
if (mapping == null)
{
foreach (var item in _mappingToEnum)
{
if (item.StringValue.Equals(value, StringComparison.OrdinalIgnoreCase))
{
mapping = item;
break;
}
}
}
if (mapping != null)
return mapping.Value;
try
{
// If no explicit mapping is found try to parse string
return (T)Enum.Parse(type, value, true);
}
catch (Exception)
{
return default;
}
}
/// <inheritdoc />
public JsonConverter CreateNullableConverter()
{
_nullableEnumConverter ??= new NullableEnumConverter(this);
return _nullableEnumConverter;
} }
} }
private static T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString, out bool warn)
{
isEmptyString = false;
warn = false;
var enumType = typeof(T);
if (_mapping == null)
_mapping = AddMapping();
var stringValue = reader.TokenType switch
{
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetInt32().ToString(),
JsonTokenType.True => reader.GetBoolean().ToString(),
JsonTokenType.False => reader.GetBoolean().ToString(),
JsonTokenType.Null => null,
_ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType)
};
if (string.IsNullOrEmpty(stringValue))
return null;
if (!GetValue(enumType, stringValue!, out var result))
{
if (string.IsNullOrWhiteSpace(stringValue))
{
isEmptyString = true;
}
else
{
// We received an enum value but weren't able to parse it.
if (!_unknownValuesWarned.Contains(stringValue))
{
warn = true;
_unknownValuesWarned.Add(stringValue!);
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {enumType.Name}, Value: {stringValue}, Known values: {string.Join(", ", _mapping.Select(m => m.Value))}. If you think {stringValue} should added please open an issue on the Github repo");
}
}
return null;
}
return result;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
var stringValue = GetString(value);
writer.WriteStringValue(stringValue);
}
private static bool GetValue(Type objectType, string value, out T? result)
{
if (_mapping != null)
{
// Check for exact match first, then if not found fallback to a case insensitive match
var mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
if (mapping.Equals(default(KeyValuePair<T, string>)))
mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
if (!mapping.Equals(default(KeyValuePair<T, string>)))
{
result = mapping.Key;
return true;
}
}
if (objectType.IsDefined(typeof(FlagsAttribute)))
{
var intValue = int.Parse(value);
result = (T)Enum.ToObject(objectType, intValue);
return true;
}
if (_unknownValuesWarned.Contains(value))
{
// Check if it is an known unknown value
// Done here to prevent lookup overhead for normal conversions, but prevent expensive exception throwing
result = default;
return false;
}
try
{
// If no explicit mapping is found try to parse string
result = (T)Enum.Parse(objectType, value, true);
return true;
}
catch (Exception)
{
result = default;
return false;
}
}
private static List<KeyValuePair<T, string>> AddMapping()
{
var mapping = new List<KeyValuePair<T, string>>();
var enumType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
var enumMembers = enumType.GetFields();
foreach (var member in enumMembers)
{
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
foreach (MapAttribute attribute in maps)
{
foreach (var value in attribute.Values)
mapping.Add(new KeyValuePair<T, string>((T)Enum.Parse(enumType, member.Name), value));
}
}
_mapping = mapping;
return mapping;
}
/// <summary>
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
/// </summary>
/// <param name="enumValue"></param>
/// <returns></returns>
[return: NotNullIfNotNull("enumValue")]
public static string? GetString(T? enumValue)
{
if (_mapping == null)
_mapping = AddMapping();
return enumValue == null ? null : (_mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
}
/// <summary>
/// Get the enum value from a string
/// </summary>
/// <param name="value">String value</param>
/// <returns></returns>
public static T? ParseString(string value)
{
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
if (_mapping == null)
_mapping = AddMapping();
var mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
if (mapping.Equals(default(KeyValuePair<T, string>)))
mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
if (!mapping.Equals(default(KeyValuePair<T, string>)))
return mapping.Key;
try
{
// If no explicit mapping is found try to parse string
return (T)Enum.Parse(type, value, true);
}
catch (Exception)
{
return default;
}
}
/// <inheritdoc />
public JsonConverter CreateNullableConverter()
{
_nullableEnumConverter ??= new NullableEnumConverter(this);
return _nullableEnumConverter;
}
} }
@@ -1,22 +1,21 @@
using System; using System;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson;
{
/// <summary>
/// Converter for serializing enum values as int
/// </summary>
public class EnumIntWriterConverter<T> : JsonConverter<T> where T: struct, Enum
{
/// <inheritdoc />
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
/// <inheritdoc /> /// <summary>
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) /// Converter for serializing enum values as int
=> writer.WriteNumberValue((int)(object)value); /// </summary>
public class EnumIntWriterConverter<T> : JsonConverter<T> where T: struct, Enum
{
/// <inheritdoc />
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
} }
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
=> writer.WriteNumberValue((int)(object)value);
} }
@@ -1,9 +1,8 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson;
internal interface INullableConverterFactory
{ {
internal interface INullableConverterFactory JsonConverter CreateNullableConverter();
{
JsonConverter CreateNullableConverter();
}
} }
@@ -1,38 +0,0 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Bool converter
/// </summary>
public class IntBoolConverter : JsonConverter<bool>
{
private readonly int _trueValue;
/// <summary>
/// ctor
/// </summary>
/// <param name="trueValue">The int value representing the true value</param>
public IntBoolConverter(int trueValue)
{
_trueValue = trueValue;
}
/// <inheritdoc />
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.Number)
return false;
return reader.GetDecimal() == _trueValue;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
{
writer.WriteNumberValue(_trueValue);
}
}
}
@@ -1,40 +1,39 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson;
/// <summary>
/// Int converter
/// </summary>
public class IntConverter : JsonConverter<int?>
{ {
/// <summary> /// <inheritdoc />
/// Int converter public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
/// </summary>
public class IntConverter : JsonConverter<int?>
{ {
/// <inheritdoc /> if (reader.TokenType == JsonTokenType.Null)
public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) return null;
if (reader.TokenType == JsonTokenType.String)
{ {
if (reader.TokenType == JsonTokenType.Null) var value = reader.GetString();
if (string.IsNullOrEmpty(value))
return null; return null;
if (reader.TokenType == JsonTokenType.String) return int.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
{
var value = reader.GetString();
if (string.IsNullOrEmpty(value))
return null;
return int.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
}
return reader.GetInt32();
} }
/// <inheritdoc /> return reader.GetInt32();
public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options) }
{
if (value == null) /// <inheritdoc />
writer.WriteNullValue(); public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options)
else {
writer.WriteNumberValue(value.Value); if (value == null)
} writer.WriteNullValue();
else
writer.WriteNumberValue(value.Value);
} }
} }
@@ -1,40 +1,39 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson;
/// <summary>
/// Int converter
/// </summary>
public class LongConverter : JsonConverter<long?>
{ {
/// <summary> /// <inheritdoc />
/// Int converter public override long? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
/// </summary>
public class LongConverter : JsonConverter<long?>
{ {
/// <inheritdoc /> if (reader.TokenType == JsonTokenType.Null)
public override long? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) return null;
if (reader.TokenType == JsonTokenType.String)
{ {
if (reader.TokenType == JsonTokenType.Null) var value = reader.GetString();
if (string.IsNullOrEmpty(value))
return null; return null;
if (reader.TokenType == JsonTokenType.String) return long.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
{
var value = reader.GetString();
if (string.IsNullOrEmpty(value))
return null;
return long.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
}
return reader.GetInt64();
} }
/// <inheritdoc /> return reader.GetInt64();
public override void Write(Utf8JsonWriter writer, long? value, JsonSerializerOptions options) }
{
if (value == null) /// <inheritdoc />
writer.WriteNullValue(); public override void Write(Utf8JsonWriter writer, long? value, JsonSerializerOptions options)
else {
writer.WriteNumberValue(value.Value); if (value == null)
} writer.WriteNullValue();
else
writer.WriteNumberValue(value.Value);
} }
} }
@@ -1,117 +0,0 @@
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
{
/// <summary>
/// JSON REST message handler
/// </summary>
public abstract class JsonRestMessageHandler : IRestMessageHandler
{
private static MediaTypeWithQualityHeaderValue _acceptJsonContent = new MediaTypeWithQualityHeaderValue(Constants.JsonContentHeader);
/// <summary>
/// Empty rate limit error
/// </summary>
protected static readonly ServerRateLimitError _emptyRateLimitError = new ServerRateLimitError();
/// <inheritdoc />
public virtual bool RequiresSeekableStream => false;
/// <summary>
/// The serializer options to use
/// </summary>
public abstract JsonSerializerOptions Options { get; }
/// <inheritdoc />
public MediaTypeWithQualityHeaderValue AcceptHeader => _acceptJsonContent;
/// <inheritdoc />
public virtual ValueTask<ServerRateLimitError> ParseErrorRateLimitResponse(
int httpStatusCode,
HttpResponseHeaders responseHeaders,
Stream responseStream)
{
// Handle retry after header
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
if (retryAfterHeader.Value?.Any() != true)
return new ValueTask<ServerRateLimitError>(_emptyRateLimitError);
var value = retryAfterHeader.Value.First();
if (int.TryParse(value, out var seconds))
return new ValueTask<ServerRateLimitError>(new ServerRateLimitError() { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) });
if (DateTime.TryParse(value, out var datetime))
return new ValueTask<ServerRateLimitError>(new ServerRateLimitError() { RetryAfter = datetime });
return new ValueTask<ServerRateLimitError>(_emptyRateLimitError);
}
/// <inheritdoc />
public abstract ValueTask<Error> ParseErrorResponse(
int httpStatusCode,
HttpResponseHeaders responseHeaders,
Stream responseStream);
/// <inheritdoc />
public virtual ValueTask<Error?> CheckForErrorResponse(
RequestDefinition request,
HttpResponseHeaders responseHeaders,
Stream responseStream) => new ValueTask<Error?>((Error?)null);
/// <summary>
/// Read the response into a JsonDocument object
/// </summary>
protected virtual async ValueTask<(Error?, JsonDocument?)> GetJsonDocument(Stream stream)
{
try
{
var document = await JsonDocument.ParseAsync(stream).ConfigureAwait(false);
return (null, document);
}
catch (Exception ex)
{
return (new ServerError(new ErrorInfo(ErrorType.DeserializationFailed, false, "Deserialization failed, invalid JSON"), ex), null);
}
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public async ValueTask<(T? Result, Error? Error)> TryDeserializeAsync<T>(Stream responseStream, CancellationToken cancellationToken)
{
try
{
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
var result = await JsonSerializer.DeserializeAsync<T>(responseStream, Options)!.ConfigureAwait(false)!;
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
return (result, null);
}
catch (JsonException ex)
{
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return (default, new DeserializeError(info, ex));
}
catch (Exception ex)
{
return (default, new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
}
/// <inheritdoc />
public virtual Error? CheckDeserializedResponse<T>(HttpResponseHeaders responseHeaders, T result) => null;
}
}
@@ -1,348 +0,0 @@
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
{
/// <summary>
/// JSON WebSocket message handler, sequentially read the JSON and looks for specific predefined fields to identify the message
/// </summary>
public abstract class JsonSocketMessageHandler : ISocketMessageHandler
{
/// <summary>
/// The serializer options to use
/// </summary>
public abstract JsonSerializerOptions Options { get; }
/// <summary>
/// Message evaluators
/// </summary>
protected abstract MessageTypeDefinition[] TypeEvaluators { get; }
private readonly SearchResult _searchResult = new();
private bool _hasArraySearches;
private bool _initialized;
private int _maxSearchDepth;
private MessageTypeDefinition? _topEvaluator;
private List<MessageEvalutorFieldReference>? _searchFields;
private Dictionary<Type, Func<object, string?>>? _baseTypeMapping;
private Dictionary<Type, Func<object, string?>>? _mapping;
/// <summary>
/// Add a mapping of a specific object of a type to a specific topic
/// </summary>
/// <typeparam name="T">Type to get topic for</typeparam>
/// <param name="mapping">The topic retrieve delegate</param>
protected void AddTopicMapping<T>(Func<T, string?> mapping)
{
_mapping ??= new Dictionary<Type, Func<object, string?>>();
_mapping.Add(typeof(T), x => mapping((T)x));
}
private void InitializeConverter()
{
if (_initialized)
return;
_maxSearchDepth = int.MinValue;
_searchFields = new List<MessageEvalutorFieldReference>();
foreach (var evaluator in TypeEvaluators)
{
_topEvaluator ??= evaluator;
foreach (var field in evaluator.Fields)
{
var overlapping = _searchFields.Where(otherField =>
{
if (field is PropertyFieldReference propRef
&& otherField.Field is PropertyFieldReference otherPropRef)
{
return field.Depth == otherPropRef.Depth && propRef.PropertyName.SequenceEqual(otherPropRef.PropertyName);
}
else if (field is ArrayFieldReference arrayRef
&& otherField.Field is ArrayFieldReference otherArrayPropRef)
{
return field.Depth == otherArrayPropRef.Depth && arrayRef.ArrayIndex == otherArrayPropRef.ArrayIndex;
}
return false;
}).ToList();
if (overlapping.Any())
{
foreach (var overlap in overlapping)
overlap.OverlappingField = true;
}
List<MessageEvalutorFieldReference>? existingSameSearchField = new();
if (field is ArrayFieldReference arrayField)
{
_hasArraySearches = true;
existingSameSearchField = _searchFields.Where(x =>
x.Field is ArrayFieldReference arrayFieldRef
&& arrayFieldRef.ArrayIndex == arrayField.ArrayIndex
&& arrayFieldRef.Depth == arrayField.Depth
&& arrayFieldRef.Constraint == null && arrayField.Constraint == null).ToList();
}
else if (field is PropertyFieldReference propField)
{
existingSameSearchField = _searchFields.Where(x =>
x.Field is PropertyFieldReference propFieldRef
&& propFieldRef.PropertyName.SequenceEqual(propField.PropertyName)
&& propFieldRef.Depth == propField.Depth
&& propFieldRef.Constraint == null && propFieldRef.Constraint == null).ToList();
}
foreach(var sameSearchField in existingSameSearchField)
{
if (sameSearchField.SkipReading == true
&& (evaluator.TypeIdentifierCallback != null || field.Constraint != null))
{
sameSearchField.SkipReading = false;
}
if (evaluator.ForceIfFound)
{
if (evaluator.Fields.Length > 1 || sameSearchField.ForceEvaluator != null)
throw new Exception("Invalid config");
//sameSearchField.ForceEvaluator = evaluator;
}
}
_searchFields.Add(new MessageEvalutorFieldReference(field)
{
SkipReading = evaluator.TypeIdentifierCallback == null && field.Constraint == null,
ForceEvaluator = !existingSameSearchField.Any() ? evaluator.ForceIfFound ? evaluator : null : null,
OverlappingField = overlapping.Any()
});
if (field.Depth > _maxSearchDepth)
_maxSearchDepth = field.Depth;
}
}
_initialized = true;
}
/// <inheritdoc />
public virtual string? GetTopicFilter(object deserializedObject)
{
if (_mapping == null)
return null;
// Cache the found type for future
var currentType = deserializedObject.GetType();
if (_baseTypeMapping != null)
{
if (_baseTypeMapping.TryGetValue(currentType, out var typeMapping))
return typeMapping(deserializedObject);
}
var mappedBase = false;
while (currentType != null)
{
if (_mapping.TryGetValue(currentType, out var mapping))
{
if (mappedBase)
{
_baseTypeMapping ??= new Dictionary<Type, Func<object, string?>>();
_baseTypeMapping.Add(deserializedObject.GetType(), mapping);
}
return mapping(deserializedObject);
}
mappedBase = true;
currentType = currentType.BaseType;
}
return null;
}
/// <inheritdoc />
public virtual string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
{
InitializeConverter();
int? arrayIndex = null;
_searchResult.Clear();
var reader = new Utf8JsonReader(data);
while (reader.Read())
{
if ((reader.TokenType == JsonTokenType.StartArray
|| reader.TokenType == JsonTokenType.StartObject)
&& reader.CurrentDepth == _maxSearchDepth)
{
// There is no field we need to search for on a depth deeper than this, skip
reader.Skip();
continue;
}
if (reader.TokenType == JsonTokenType.StartArray)
arrayIndex = -1;
else if (reader.TokenType == JsonTokenType.EndArray)
arrayIndex = null;
else if (arrayIndex != null)
arrayIndex++;
if (reader.TokenType == JsonTokenType.PropertyName
|| arrayIndex != null && _hasArraySearches)
{
bool written = false;
string? value = null;
byte[]? propName = null;
foreach (var field in _searchFields!)
{
if (field.Field.Depth != reader.CurrentDepth)
continue;
bool readArrayValues = false;
if (field.Field is PropertyFieldReference propFieldRef)
{
if (propName == null)
{
if (reader.TokenType != JsonTokenType.PropertyName)
continue;
if (!reader.ValueTextEquals(propFieldRef.PropertyName))
continue;
propName = propFieldRef.PropertyName;
readArrayValues = propFieldRef.ArrayValues;
reader.Read();
}
else if (!propFieldRef.PropertyName.SequenceEqual(propName))
{
continue;
}
}
else if (field.Field is ArrayFieldReference arrayFieldRef)
{
if (propName != null)
continue;
if (reader.TokenType == JsonTokenType.PropertyName)
continue;
if (arrayFieldRef.ArrayIndex != arrayIndex)
continue;
}
if (!field.SkipReading)
{
if (value == null)
{
if (readArrayValues)
{
if (reader.TokenType != JsonTokenType.StartArray)
// error
return null;
var sb = new StringBuilder();
reader.Read();// Read start array
bool first = true;
while(reader.TokenType != JsonTokenType.EndArray)
{
if (!first)
sb.Append(",");
first = false;
sb.Append(reader.GetString());
reader.Read();
}
value = first ? null : sb.ToString();
}
else
{
switch (reader.TokenType)
{
case JsonTokenType.Number:
value = reader.GetDecimal().ToString();
break;
case JsonTokenType.String:
value = reader.GetString()!;
break;
case JsonTokenType.True:
case JsonTokenType.False:
value = reader.GetBoolean().ToString()!;
break;
case JsonTokenType.Null:
value = null;
break;
case JsonTokenType.StartObject:
case JsonTokenType.StartArray:
value = null;
break;
default:
continue;
}
}
}
if (field.Field.Constraint != null
&& !field.Field.Constraint(value))
{
continue;
}
}
_searchResult.Write(field.Field, value);
if (field.ForceEvaluator != null)
{
if (field.ForceEvaluator.StaticIdentifier != null)
return field.ForceEvaluator.StaticIdentifier;
// Force the immediate return upon encountering this field
return field.ForceEvaluator.GetMessageType(_searchResult);
}
written = true;
if (!field.OverlappingField)
break;
}
if (!written)
continue;
if (_topEvaluator!.Satisfied(_searchResult))
return _topEvaluator.GetMessageType(_searchResult);
if (_searchFields.Count == _searchResult.Count)
break;
}
}
foreach (var evaluator in TypeEvaluators)
{
if (evaluator.Satisfied(_searchResult))
return evaluator.GetMessageType(_searchResult);
}
return null;
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public virtual object Deserialize(ReadOnlySpan<byte> data, Type type)
{
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
return JsonSerializer.Deserialize(data, type, Options)!;
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
}
}
}
@@ -1,63 +0,0 @@
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using System;
using System.Net.WebSockets;
using System.Text.Json;
namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
{
/// <summary>
/// JSON WebSocket message handler, reads the json data info a JsonDocument after which the data can be inspected to identify the message
/// </summary>
public abstract class JsonSocketPreloadMessageHandler : ISocketMessageHandler
{
/// <summary>
/// The serializer options to use
/// </summary>
public abstract JsonSerializerOptions Options { get; }
/// <inheritdoc />
public virtual string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
{
var reader = new Utf8JsonReader(data);
var jsonDocument = JsonDocument.ParseValue(ref reader);
return GetTypeIdentifier(jsonDocument);
}
/// <summary>
/// Get the message identifier for this document
/// </summary>
protected abstract string? GetTypeIdentifier(JsonDocument document);
/// <summary>
/// Get optional topic filter, for example a symbol name
/// </summary>
public virtual string? GetTopicFilter(object deserializedObject) => null;
/// <inheritdoc />
public virtual object Deserialize(ReadOnlySpan<byte> data, Type type)
{
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
return JsonSerializer.Deserialize(data, type, Options)!;
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
}
/// <summary>
/// Get the string value for a path, or an emtpy string if not found
/// </summary>
protected string StringOrEmpty(JsonDocument document, string path)
{
if (!document.RootElement.TryGetProperty(path, out var element))
return string.Empty;
if (element.ValueKind == JsonValueKind.String)
return element.GetString() ?? string.Empty;
else if (element.ValueKind == JsonValueKind.Number)
return element.GetDecimal().ToString();
return string.Empty;
}
}
}
@@ -1,41 +1,40 @@
using System; using System;
using System.Text.Json.Serialization.Metadata; using System.Text.Json.Serialization.Metadata;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson;
internal class NullableEnumConverterFactory : JsonConverterFactory
{ {
internal class NullableEnumConverterFactory : JsonConverterFactory private readonly IJsonTypeInfoResolver _jsonTypeInfoResolver;
private static readonly JsonSerializerOptions _options = new JsonSerializerOptions();
public NullableEnumConverterFactory(IJsonTypeInfoResolver jsonTypeInfoResolver)
{ {
private readonly IJsonTypeInfoResolver _jsonTypeInfoResolver; _jsonTypeInfoResolver = jsonTypeInfoResolver;
private static readonly JsonSerializerOptions _options = new JsonSerializerOptions(); }
public NullableEnumConverterFactory(IJsonTypeInfoResolver jsonTypeInfoResolver) public override bool CanConvert(Type typeToConvert)
{ {
_jsonTypeInfoResolver = jsonTypeInfoResolver; var b = Nullable.GetUnderlyingType(typeToConvert);
} if (b == null)
return false;
public override bool CanConvert(Type typeToConvert) var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(b, _options);
{ if (typeInfo == null)
var b = Nullable.GetUnderlyingType(typeToConvert); return false;
if (b == null)
return false;
var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(b, _options); return typeInfo.Converter is INullableConverterFactory;
if (typeInfo == null) }
return false;
return typeInfo.Converter is INullableConverterFactory; public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
} {
var b = Nullable.GetUnderlyingType(typeToConvert) ?? throw new ArgumentNullException($"Not nullable {typeToConvert.Name}");
var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(b, _options) ?? throw new ArgumentNullException($"Can find type {typeToConvert.Name}");
if (typeInfo.Converter is not INullableConverterFactory nullConverterFactory)
throw new ArgumentNullException($"Can find type converter for {typeToConvert.Name}");
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) return nullConverterFactory.CreateNullableConverter();
{
var b = Nullable.GetUnderlyingType(typeToConvert) ?? throw new ArgumentNullException($"Not nullable {typeToConvert.Name}");
var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(b, _options) ?? throw new ArgumentNullException($"Can find type {typeToConvert.Name}");
if (typeInfo.Converter is not INullableConverterFactory nullConverterFactory)
throw new ArgumentNullException($"Can find type converter for {typeToConvert.Name}");
return nullConverterFactory.CreateNullableConverter();
}
} }
} }
@@ -1,42 +1,41 @@
using System; using System;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson;
/// <summary>
/// Read string or number as string
/// </summary>
public class NumberStringConverter : JsonConverter<string?>
{ {
/// <summary> /// <inheritdoc />
/// Read string or number as string public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
/// </summary>
public class NumberStringConverter : JsonConverter<string?>
{ {
/// <inheritdoc /> if (reader.TokenType == JsonTokenType.Null)
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) return null;
if (reader.TokenType == JsonTokenType.Number)
{ {
if (reader.TokenType == JsonTokenType.Null) if (reader.TryGetInt64(out var value))
return null; return value.ToString();
if (reader.TokenType == JsonTokenType.Number) return reader.GetDecimal().ToString();
{
if (reader.TryGetInt64(out var value))
return value.ToString();
return reader.GetDecimal().ToString();
}
try
{
return reader.GetString();
}
catch (Exception)
{
return null;
}
} }
/// <inheritdoc /> try
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
{ {
writer.WriteStringValue(value); return reader.GetString();
}
catch (Exception)
{
return null;
} }
} }
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
{
writer.WriteStringValue(value);
}
} }
@@ -1,58 +0,0 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter for parsing object or array responses
/// </summary>
public class ObjectOrArrayConverter : JsonConverterFactory
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert) => true;
/// <inheritdoc />
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var type = typeof(InternalObjectOrArrayConverter<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(type)!;
}
private class InternalObjectOrArrayConverter<T> : JsonConverter<T>
{
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.StartObject && !typeToConvert.IsArray)
{
// Object to object
return JsonDocument.ParseValue(ref reader).Deserialize<T>(options);
}
else if (reader.TokenType == JsonTokenType.StartArray && typeToConvert.IsArray)
{
// Array to array
return JsonDocument.ParseValue(ref reader).Deserialize<T>(options);
}
else if (reader.TokenType == JsonTokenType.StartArray)
{
// Array to object
JsonDocument.ParseValue(ref reader).Deserialize<T[]>(options);
return default;
}
else
{
// Object to array
JsonDocument.ParseValue(ref reader);
return default;
}
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, options);
}
}
}
}
@@ -1,43 +1,44 @@
using System; using System;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Text.Json; using System.Text.Json;
#if NET5_0_OR_GREATER
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
#endif
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson;
/// <summary>
/// Converter for values which contain a nested json value
/// </summary>
public class ObjectStringConverter<T> : JsonConverter<T>
{ {
/// <summary> /// <inheritdoc />
/// Converter for values which contain a nested json value #if NET5_0_OR_GREATER
/// </summary> [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
public class ObjectStringConverter<T> : JsonConverter<T> [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
/// <inheritdoc /> if (reader.TokenType == JsonTokenType.Null)
return default;
var value = reader.GetString();
if (string.IsNullOrEmpty(value))
return default;
return JsonDocument.Parse(value!).Deserialize<T>(options);
}
/// <inheritdoc />
#if NET5_0_OR_GREATER #if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif #endif
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
{ {
if (reader.TokenType == JsonTokenType.Null) if (value is null)
return default; writer.WriteStringValue("");
var value = reader.GetString(); writer.WriteStringValue(JsonSerializer.Serialize(value, options));
if (string.IsNullOrEmpty(value))
return default;
return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T), options);
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
{
if (value is null)
writer.WriteStringValue("");
writer.WriteStringValue(JsonSerializer.Serialize(value, options));
}
} }
} }
@@ -1,41 +1,40 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson;
/// <summary>
/// Replace a value on a string property
/// </summary>
public abstract class ReplaceConverter : JsonConverter<string>
{ {
private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets;
/// <summary> /// <summary>
/// Replace a value on a string property /// ctor
/// </summary> /// </summary>
public abstract class ReplaceConverter : JsonConverter<string> public ReplaceConverter(params string[] replaceSets)
{ {
private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets; _replacementSets = replaceSets.Select(x =>
/// <summary>
/// ctor
/// </summary>
public ReplaceConverter(params string[] replaceSets)
{ {
_replacementSets = replaceSets.Select(x => var split = x.Split(["->"], StringSplitOptions.None);
{ if (split.Length != 2)
var split = x.Split(new string[] { "->" }, StringSplitOptions.None); throw new ArgumentException("Invalid replacement config");
if (split.Length != 2) return (split[0], split[1]);
throw new ArgumentException("Invalid replacement config"); }).ToArray();
return (split[0], split[1]);
}).ToArray();
}
/// <inheritdoc />
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var value = reader.GetString();
foreach (var set in _replacementSets)
value = value?.Replace(set.ValueToReplace, set.ValueToReplaceWith);
return value;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => writer.WriteStringValue(value);
} }
/// <inheritdoc />
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var value = reader.GetString();
foreach (var set in _replacementSets)
value = value?.Replace(set.ValueToReplace, set.ValueToReplaceWith);
return value;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => writer.WriteStringValue(value);
} }
@@ -1,21 +1,20 @@
using System; using System;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson;
/// <summary>
/// Attribute to mark a model as json serializable. Used for AOT compilation.
/// </summary>
[AttributeUsage(System.AttributeTargets.Class | AttributeTargets.Enum | System.AttributeTargets.Interface)]
public class SerializationModelAttribute : Attribute
{ {
/// <summary> /// <summary>
/// Attribute to mark a model as json serializable. Used for AOT compilation. /// ctor
/// </summary> /// </summary>
[AttributeUsage(System.AttributeTargets.Class | AttributeTargets.Enum | System.AttributeTargets.Interface)] public SerializationModelAttribute() { }
public class SerializationModelAttribute : Attribute /// <summary>
{ /// ctor
/// <summary> /// </summary>
/// ctor /// <param name="type"></param>
/// </summary> public SerializationModelAttribute(Type type) { }
public SerializationModelAttribute() { }
/// <summary>
/// ctor
/// </summary>
/// <param name="type"></param>
public SerializationModelAttribute(Type type) { }
}
} }
@@ -1,47 +1,46 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson;
/// <summary>
/// Serializer options
/// </summary>
public static class SerializerOptions
{ {
private static readonly ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions> _cache = new ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions>();
/// <summary> /// <summary>
/// Serializer options /// Get Json serializer settings which includes standard converters for DateTime, bool, enum and number types
/// </summary> /// </summary>
public static class SerializerOptions public static JsonSerializerOptions WithConverters(JsonSerializerContext typeResolver, params JsonConverter[] additionalConverters)
{ {
private static readonly ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions> _cache = new ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions>(); if (!_cache.TryGetValue(typeResolver, out var options))
/// <summary>
/// Get Json serializer settings which includes standard converters for DateTime, bool, enum and number types
/// </summary>
public static JsonSerializerOptions WithConverters(JsonSerializerContext typeResolver, params JsonConverter[] additionalConverters)
{ {
if (!_cache.TryGetValue(typeResolver, out var options)) options = new JsonSerializerOptions
{ {
options = new JsonSerializerOptions NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
PropertyNameCaseInsensitive = false,
Converters =
{ {
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals, new DateTimeConverter(),
PropertyNameCaseInsensitive = false, new BoolConverter(),
Converters = new DecimalConverter(),
{ new IntConverter(),
new DateTimeConverter(), new LongConverter(),
new BoolConverter(), new NullableEnumConverterFactory(typeResolver)
new DecimalConverter(), },
new IntConverter(), TypeInfoResolver = typeResolver,
new LongConverter(), };
new NullableEnumConverterFactory(typeResolver)
},
TypeInfoResolver = typeResolver,
};
foreach (var converter in additionalConverters) foreach (var converter in additionalConverters)
options.Converters.Add(converter); options.Converters.Add(converter);
options.TypeInfoResolver = typeResolver; options.TypeInfoResolver = typeResolver;
_cache.TryAdd(typeResolver, options); _cache.TryAdd(typeResolver, options);
}
return options;
} }
return options;
} }
} }
@@ -1,58 +1,57 @@
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
using System; using System;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson;
internal class SharedQuantityConverter : SharedQuantityReferenceConverter<SharedQuantity> { }
internal class SharedOrderQuantityConverter : SharedQuantityReferenceConverter<SharedOrderQuantity> { }
internal class SharedQuantityReferenceConverter<T> : JsonConverter<T> where T: SharedQuantityReference, new()
{ {
internal class SharedQuantityConverter : SharedQuantityReferenceConverter<SharedQuantity> { } public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
internal class SharedOrderQuantityConverter : SharedQuantityReferenceConverter<SharedOrderQuantity> { }
internal class SharedQuantityReferenceConverter<T> : JsonConverter<T> where T: SharedQuantityReference, new()
{ {
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) if (reader.TokenType != JsonTokenType.StartArray)
{ throw new Exception("");
if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception("");
reader.Read(); // Start array reader.Read(); // Start array
var baseQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal(); var baseQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
reader.Read(); reader.Read();
var quoteQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal(); var quoteQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
reader.Read(); reader.Read();
var contractQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal(); var contractQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
reader.Read(); reader.Read();
if (reader.TokenType != JsonTokenType.EndArray) if (reader.TokenType != JsonTokenType.EndArray)
throw new Exception(""); throw new Exception("");
reader.Read(); // End array reader.Read(); // End array
var result = new T(); var result = new T();
result.QuantityInBaseAsset = baseQuantity; result.QuantityInBaseAsset = baseQuantity;
result.QuantityInQuoteAsset = quoteQuantity; result.QuantityInQuoteAsset = quoteQuantity;
result.QuantityInContracts = contractQuantity; result.QuantityInContracts = contractQuantity;
return result; return result;
} }
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{ {
writer.WriteStartArray(); writer.WriteStartArray();
if (value.QuantityInBaseAsset == null) if (value.QuantityInBaseAsset == null)
writer.WriteNullValue(); writer.WriteNullValue();
else else
writer.WriteNumberValue(value.QuantityInBaseAsset.Value); writer.WriteNumberValue(value.QuantityInBaseAsset.Value);
if (value.QuantityInQuoteAsset == null) if (value.QuantityInQuoteAsset == null)
writer.WriteNullValue(); writer.WriteNullValue();
else else
writer.WriteNumberValue(value.QuantityInQuoteAsset.Value); writer.WriteNumberValue(value.QuantityInQuoteAsset.Value);
if (value.QuantityInContracts == null) if (value.QuantityInContracts == null)
writer.WriteNullValue(); writer.WriteNullValue();
else else
writer.WriteNumberValue(value.QuantityInContracts.Value); writer.WriteNumberValue(value.QuantityInContracts.Value);
writer.WriteEndArray(); writer.WriteEndArray();
}
} }
} }
@@ -1,44 +1,43 @@
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
using System; using System;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson;
internal class SharedSymbolConverter : JsonConverter<SharedSymbol>
{ {
internal class SharedSymbolConverter : JsonConverter<SharedSymbol> public override SharedSymbol? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
public override SharedSymbol? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) if (reader.TokenType != JsonTokenType.StartArray)
{ throw new Exception("");
if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception("");
reader.Read(); // Start array reader.Read(); // Start array
var tradingMode = (TradingMode)Enum.Parse(typeof(TradingMode), reader.GetString()!); var tradingMode = (TradingMode)Enum.Parse(typeof(TradingMode), reader.GetString()!);
reader.Read(); reader.Read();
var baseAsset = reader.GetString()!; var baseAsset = reader.GetString()!;
reader.Read(); reader.Read();
var quoteAsset = reader.GetString()!; var quoteAsset = reader.GetString()!;
reader.Read(); reader.Read();
var timeStr = reader.GetString()!; var timeStr = reader.GetString()!;
var deliverTime = string.IsNullOrEmpty(timeStr) ? (DateTime?)null : DateTime.Parse(timeStr); var deliverTime = string.IsNullOrEmpty(timeStr) ? (DateTime?)null : DateTime.Parse(timeStr);
reader.Read(); reader.Read();
if (reader.TokenType != JsonTokenType.EndArray) if (reader.TokenType != JsonTokenType.EndArray)
throw new Exception(""); throw new Exception("");
reader.Read(); // End array reader.Read(); // End array
return new SharedSymbol(tradingMode, baseAsset, quoteAsset, deliverTime); return new SharedSymbol(tradingMode, baseAsset, quoteAsset, deliverTime);
} }
public override void Write(Utf8JsonWriter writer, SharedSymbol value, JsonSerializerOptions options) public override void Write(Utf8JsonWriter writer, SharedSymbol value, JsonSerializerOptions options)
{ {
writer.WriteStartArray(); writer.WriteStartArray();
writer.WriteStringValue(value.TradingMode.ToString()); writer.WriteStringValue(value.TradingMode.ToString());
writer.WriteStringValue(value.BaseAsset); writer.WriteStringValue(value.BaseAsset);
writer.WriteStringValue(value.QuoteAsset); writer.WriteStringValue(value.QuoteAsset);
writer.WriteStringValue(value.DeliverTime?.ToString()); writer.WriteStringValue(value.DeliverTime?.ToString());
writer.WriteEndArray(); writer.WriteEndArray();
}
} }
} }
@@ -1,373 +1,376 @@
using CryptoExchange.Net.Converters.MessageParsing; using CryptoExchange.Net.Converters.MessageParsing;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using System; using System;
#if NET5_0_OR_GREATER
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
#endif
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson;
/// <summary>
/// System.Text.Json message accessor
/// </summary>
public abstract class SystemTextJsonMessageAccessor : IMessageAccessor
{ {
/// <summary> /// <summary>
/// System.Text.Json message accessor /// The JsonDocument loaded
/// </summary> /// </summary>
public abstract class SystemTextJsonMessageAccessor : IMessageAccessor protected JsonDocument? _document;
{
/// <summary>
/// The JsonDocument loaded
/// </summary>
protected JsonDocument? _document;
private readonly JsonSerializerOptions? _customSerializerOptions; private readonly JsonSerializerOptions? _customSerializerOptions;
/// <inheritdoc /> /// <inheritdoc />
public bool IsValid { get; set; } public bool IsValid { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public abstract bool OriginalDataAvailable { get; } public abstract bool OriginalDataAvailable { get; }
/// <inheritdoc /> /// <inheritdoc />
public object? Underlying => throw new NotImplementedException(); public object? Underlying => throw new NotImplementedException();
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonMessageAccessor(JsonSerializerOptions options)
{
_customSerializerOptions = options;
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
{
if (!IsValid)
return new CallResult<object>(GetOriginalString());
if (_document == null)
throw new InvalidOperationException("No json document loaded");
try
{
var result = _document.Deserialize(type, _customSerializerOptions);
return new CallResult<object>(result!);
}
catch (JsonException ex)
{
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return new CallResult<object>(new DeserializeError(info, ex));
}
catch (Exception ex)
{
return new CallResult<object>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public CallResult<T> Deserialize<T>(MessagePath? path = null)
{
if (_document == null)
throw new InvalidOperationException("No json document loaded");
try
{
var result = _document.Deserialize<T>(_customSerializerOptions);
return new CallResult<T>(result!);
}
catch (JsonException ex)
{
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return new CallResult<T>(new DeserializeError(info, ex));
}
catch (Exception ex)
{
return new CallResult<T>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
}
/// <inheritdoc />
public NodeType? GetNodeType()
{
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
if (_document == null)
throw new InvalidOperationException("No json document loaded");
return _document.RootElement.ValueKind switch
{
JsonValueKind.Object => NodeType.Object,
JsonValueKind.Array => NodeType.Array,
_ => NodeType.Value
};
}
/// <inheritdoc />
public NodeType? GetNodeType(MessagePath path)
{
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
var node = GetPathNode(path);
if (!node.HasValue)
return null;
return node.Value.ValueKind switch
{
JsonValueKind.Object => NodeType.Object,
JsonValueKind.Array => NodeType.Array,
_ => NodeType.Value
};
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public T? GetValue<T>(MessagePath path)
{
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
var value = GetPathNode(path);
if (value == null)
return default;
if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array)
{
try
{
return value.Value.Deserialize<T>(_customSerializerOptions);
}
catch { }
return default;
}
if (typeof(T) == typeof(string))
{
if (value.Value.ValueKind == JsonValueKind.Number)
return (T)(object)value.Value.GetInt64().ToString();
}
return value.Value.Deserialize<T>(_customSerializerOptions);
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public T?[]? GetValues<T>(MessagePath path)
{
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
var value = GetPathNode(path);
if (value == null)
return default;
if (value.Value.ValueKind != JsonValueKind.Array)
return default;
return value.Value.Deserialize<T[]>(_customSerializerOptions)!;
}
private JsonElement? GetPathNode(MessagePath path)
{
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
if (_document == null)
throw new InvalidOperationException("No json document loaded");
JsonElement? currentToken = _document.RootElement;
foreach (var node in path)
{
if (node.Type == 0)
{
// Int value
var val = node.Index!.Value;
if (currentToken!.Value.ValueKind != JsonValueKind.Array || currentToken.Value.GetArrayLength() <= val)
return null;
currentToken = currentToken.Value[val];
}
else if (node.Type == 1)
{
// String value
if (currentToken!.Value.ValueKind != JsonValueKind.Object)
return null;
if (!currentToken.Value.TryGetProperty(node.Property!, out var token))
return null;
currentToken = token;
}
else
{
// Property name
if (currentToken!.Value.ValueKind != JsonValueKind.Object)
return null;
throw new NotImplementedException();
}
if (currentToken == null)
return null;
}
return currentToken;
}
/// <inheritdoc />
public abstract string GetOriginalString();
/// <inheritdoc />
public abstract void Clear();
}
/// <summary> /// <summary>
/// System.Text.Json stream message accessor /// ctor
/// </summary> /// </summary>
public class SystemTextJsonStreamMessageAccessor : SystemTextJsonMessageAccessor, IStreamMessageAccessor public SystemTextJsonMessageAccessor(JsonSerializerOptions options)
{ {
private Stream? _stream; _customSerializerOptions = options;
}
/// <inheritdoc /> /// <inheritdoc />
public override bool OriginalDataAvailable => _stream?.CanSeek == true; #if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
{
if (!IsValid)
return new CallResult<object>(GetOriginalString());
/// <summary> if (_document == null)
/// ctor throw new InvalidOperationException("No json document loaded");
/// </summary>
public SystemTextJsonStreamMessageAccessor(JsonSerializerOptions options): base(options) try
{ {
var result = _document.Deserialize(type, _customSerializerOptions);
return new CallResult<object>(result!);
}
catch (JsonException ex)
{
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return new CallResult<object>(new DeserializeError(info, ex));
}
catch (Exception ex)
{
return new CallResult<object>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public CallResult<T> Deserialize<T>(MessagePath? path = null)
{
if (_document == null)
throw new InvalidOperationException("No json document loaded");
try
{
var result = _document.Deserialize<T>(_customSerializerOptions);
return new CallResult<T>(result!);
}
catch (JsonException ex)
{
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return new CallResult<T>(new DeserializeError(info, ex));
}
catch (Exception ex)
{
return new CallResult<T>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
}
/// <inheritdoc />
public NodeType? GetNodeType()
{
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
if (_document == null)
throw new InvalidOperationException("No json document loaded");
return _document.RootElement.ValueKind switch
{
JsonValueKind.Object => NodeType.Object,
JsonValueKind.Array => NodeType.Array,
_ => NodeType.Value
};
}
/// <inheritdoc />
public NodeType? GetNodeType(MessagePath path)
{
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
var node = GetPathNode(path);
if (!node.HasValue)
return null;
return node.Value.ValueKind switch
{
JsonValueKind.Object => NodeType.Object,
JsonValueKind.Array => NodeType.Array,
_ => NodeType.Value
};
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public T? GetValue<T>(MessagePath path)
{
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
var value = GetPathNode(path);
if (value == null)
return default;
if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array)
{
try
{
return value.Value.Deserialize<T>(_customSerializerOptions);
}
catch { }
return default;
} }
/// <inheritdoc /> if (typeof(T) == typeof(string))
public async Task<CallResult> Read(Stream stream, bool bufferStream)
{ {
if (bufferStream && stream is not MemoryStream) if (value.Value.ValueKind == JsonValueKind.Number)
return (T)(object)value.Value.GetInt64().ToString();
}
return value.Value.Deserialize<T>(_customSerializerOptions);
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public T?[]? GetValues<T>(MessagePath path)
{
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
var value = GetPathNode(path);
if (value == null)
return default;
if (value.Value.ValueKind != JsonValueKind.Array)
return default;
return value.Value.Deserialize<T[]>(_customSerializerOptions)!;
}
private JsonElement? GetPathNode(MessagePath path)
{
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
if (_document == null)
throw new InvalidOperationException("No json document loaded");
JsonElement? currentToken = _document.RootElement;
foreach (var node in path)
{
if (node.Type == 0)
{ {
// We need to be buffer the stream, and it's not currently a seekable stream, so copy it to a new memory stream // Int value
_stream = new MemoryStream(); var val = node.Index!.Value;
stream.CopyTo(_stream); if (currentToken!.Value.ValueKind != JsonValueKind.Array || currentToken.Value.GetArrayLength() <= val)
_stream.Position = 0; return null;
currentToken = currentToken.Value[val];
} }
else if (bufferStream) else if (node.Type == 1)
{ {
// We need to buffer the stream, and the current stream is seekable, store as is // String value
_stream = stream; if (currentToken!.Value.ValueKind != JsonValueKind.Object)
return null;
if (!currentToken.Value.TryGetProperty(node.Property!, out var token))
return null;
currentToken = token;
} }
else else
{ {
// We don't need to buffer the stream, so don't bother keeping the reference // Property name
if (currentToken!.Value.ValueKind != JsonValueKind.Object)
return null;
throw new NotImplementedException();
} }
try if (currentToken == null)
{ return null;
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
IsValid = true;
return CallResult.SuccessResult;
}
catch (Exception ex)
{
// Not a json message
IsValid = false;
return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
}
/// <inheritdoc />
public override string GetOriginalString()
{
if (_stream is null)
throw new NullReferenceException("Stream not initialized");
_stream.Position = 0;
using var textReader = new StreamReader(_stream, Encoding.UTF8, false, 1024, true);
return textReader.ReadToEnd();
}
/// <inheritdoc />
public override void Clear()
{
_stream?.Dispose();
_stream = null;
_document?.Dispose();
_document = null;
} }
return currentToken;
} }
/// <inheritdoc />
public abstract string GetOriginalString();
/// <inheritdoc />
public abstract void Clear();
}
/// <summary>
/// System.Text.Json stream message accessor
/// </summary>
#pragma warning disable CA1001 // Types that own disposable fields should be disposable
public class SystemTextJsonStreamMessageAccessor : SystemTextJsonMessageAccessor, IStreamMessageAccessor
#pragma warning restore CA1001 // Types that own disposable fields should be disposable
{
private Stream? _stream;
/// <inheritdoc />
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
/// <summary> /// <summary>
/// System.Text.Json byte message accessor /// ctor
/// </summary> /// </summary>
public class SystemTextJsonByteMessageAccessor : SystemTextJsonMessageAccessor, IByteMessageAccessor public SystemTextJsonStreamMessageAccessor(JsonSerializerOptions options): base(options)
{ {
private ReadOnlyMemory<byte> _bytes; }
/// <summary> /// <inheritdoc />
/// ctor public async Task<CallResult> Read(Stream stream, bool bufferStream)
/// </summary> {
public SystemTextJsonByteMessageAccessor(JsonSerializerOptions options) : base(options) 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
} }
/// <inheritdoc /> try
public CallResult Read(ReadOnlyMemory<byte> data)
{ {
_bytes = data; _document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
IsValid = true;
try return CallResult.SuccessResult;
{
var firstByte = data.Span[0];
if (firstByte != 0x7b && firstByte != 0x5b)
{
// Value doesn't start with `{` or `[`, prevent deserialization attempt as it's slow
IsValid = false;
return new CallResult(new DeserializeError("Not a json value"));
}
_document = JsonDocument.Parse(data);
IsValid = true;
return CallResult.SuccessResult;
}
catch (Exception ex)
{
// Not a json message
IsValid = false;
return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
} }
catch (Exception 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; // Not a json message
_document?.Dispose(); IsValid = false;
_document = null; return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
} }
} }
/// <inheritdoc />
public override string GetOriginalString()
{
if (_stream is null)
throw new NullReferenceException("Stream not initialized");
_stream.Position = 0;
using var textReader = new StreamReader(_stream, Encoding.UTF8, false, 1024, true);
return textReader.ReadToEnd();
}
/// <inheritdoc />
public override void Clear()
{
_stream?.Dispose();
_stream = null;
_document?.Dispose();
_document = null;
}
}
/// <summary>
/// System.Text.Json byte message accessor
/// </summary>
public class SystemTextJsonByteMessageAccessor : SystemTextJsonMessageAccessor, IByteMessageAccessor
{
private ReadOnlyMemory<byte> _bytes;
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonByteMessageAccessor(JsonSerializerOptions options) : base(options)
{
}
/// <inheritdoc />
public CallResult Read(ReadOnlyMemory<byte> data)
{
_bytes = data;
try
{
var firstByte = data.Span[0];
if (firstByte != 0x7b && firstByte != 0x5b)
{
// Value doesn't start with `{` or `[`, prevent deserialization attempt as it's slow
IsValid = false;
return new CallResult(new DeserializeError("Not a json value"));
}
_document = JsonDocument.Parse(data);
IsValid = true;
return CallResult.SuccessResult;
}
catch (Exception ex)
{
// Not a json message
IsValid = false;
return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
}
/// <inheritdoc />
public override string GetOriginalString() =>
// NetStandard 2.0 doesn't support GetString from a ReadonlySpan<byte>, so use ToArray there instead
#if NETSTANDARD2_0
Encoding.UTF8.GetString(_bytes.ToArray());
#else
Encoding.UTF8.GetString(_bytes.Span);
#endif
/// <inheritdoc />
public override bool OriginalDataAvailable => true;
/// <inheritdoc />
public override void Clear()
{
_bytes = null;
_document?.Dispose();
_document = null;
}
} }
@@ -1,27 +1,28 @@
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
#if NET5_0_OR_GREATER
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
#endif
using System.Text.Json; using System.Text.Json;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson;
/// <inheritdoc />
public class SystemTextJsonMessageSerializer : IStringMessageSerializer
{ {
/// <inheritdoc /> private readonly JsonSerializerOptions _options;
public class SystemTextJsonMessageSerializer : IStringMessageSerializer
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonMessageSerializer(JsonSerializerOptions options)
{ {
private readonly JsonSerializerOptions _options; _options = options;
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonMessageSerializer(JsonSerializerOptions options)
{
_options = options;
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
#endif
public string Serialize<T>(T message) => JsonSerializer.Serialize(message, _options);
} }
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
#endif
public string Serialize<T>(T message) => JsonSerializer.Serialize(message, _options);
} }
+20 -12
View File
@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0</TargetFrameworks> <TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<PackageId>CryptoExchange.Net</PackageId> <PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors> <Authors>JKorf</Authors>
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description> <Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
<PackageVersion>10.2.5</PackageVersion> <PackageVersion>9.6.0</PackageVersion>
<AssemblyVersion>10.2.5</AssemblyVersion> <AssemblyVersion>9.6.0</AssemblyVersion>
<FileVersion>10.2.5</FileVersion> <FileVersion>9.6.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags> <PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
@@ -20,10 +20,11 @@
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<LangVersion>latest</LangVersion> <LangVersion>12.0</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Include="C:\Projects\CryptoExchange.Net\CryptoExchange.Net\.editorconfig" />
<None Include="Icon\icon.png" Pack="true" PackagePath="\" /> <None Include="Icon\icon.png" Pack="true" PackagePath="\" />
<None Include="..\README.md" Pack="true" PackagePath="\" /> <None Include="..\README.md" Pack="true" PackagePath="\" />
</ItemGroup> </ItemGroup>
@@ -40,24 +41,31 @@
<PropertyGroup> <PropertyGroup>
<DocumentationFile>CryptoExchange.Net.xml</DocumentationFile> <DocumentationFile>CryptoExchange.Net.xml</DocumentationFile>
</PropertyGroup> </PropertyGroup>
<PropertyGroup>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisMode>Recommended</AnalysisMode>
<AnalysisModeGlobalization>None</AnalysisModeGlobalization>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0.1"> <PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.101"> <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.6" />
<PackageReference Include="System.Text.Json" Version="10.0.1" /> <PackageReference Include="System.Text.Json" Version="9.0.6" />
<PackageReference Include="NSec.Cryptography" Version="25.4.0" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Client Packages"> <ItemGroup Label="Transitive Client Packages">
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.6" />
<PackageReference Include="System.Threading.Channels" Version="10.0.1" /> </ItemGroup>
<ItemGroup>
<EditorConfigFiles Remove="C:\Projects\CryptoExchange.Net\CryptoExchange.Net\.editorconfig" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -1,24 +0,0 @@
using System;
namespace CryptoExchange.Net.Exceptions
{
/// <summary>
/// Exception during deserialization
/// </summary>
public class CeDeserializationException : Exception
{
/// <summary>
/// ctor
/// </summary>
public CeDeserializationException(string message) : base(message)
{
}
/// <summary>
/// ctor
/// </summary>
public CeDeserializationException(string message, Exception innerException) : base(message, innerException)
{
}
}
}
+339 -453
View File
@@ -1,504 +1,390 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
using System.Security.Cryptography; using System.Security.Cryptography;
#endif
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace CryptoExchange.Net namespace CryptoExchange.Net;
/// <summary>
/// General helpers functions
/// </summary>
public static class ExchangeHelpers
{ {
/// <summary> private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789";
/// General helpers functions private const string _allowedRandomHexChars = "0123456789ABCDEF";
/// </summary>
public static class ExchangeHelpers private static readonly Dictionary<int, string> _monthSymbols = new Dictionary<int, string>()
{ {
private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789"; { 1, "F" },
private const string _allowedRandomHexChars = "0123456789ABCDEF"; { 2, "G" },
{ 3, "H" },
{ 4, "J" },
{ 5, "K" },
{ 6, "M" },
{ 7, "N" },
{ 8, "Q" },
{ 9, "U" },
{ 10, "V" },
{ 11, "X" },
{ 12, "Z" },
};
private static readonly Dictionary<int, string> _monthSymbols = new Dictionary<int, string>() /// <summary>
{ /// The last used id, use NextId() to get the next id and up this
{ 1, "F" }, /// </summary>
{ 2, "G" }, private static int _lastId;
{ 3, "H" },
{ 4, "J" },
{ 5, "K" },
{ 6, "M" },
{ 7, "N" },
{ 8, "Q" },
{ 9, "U" },
{ 10, "V" },
{ 11, "X" },
{ 12, "Z" },
};
/// <summary> /// <summary>
/// The last used id, use NextId() to get the next id and up this /// Clamp a value between a min and max
/// </summary> /// </summary>
private static int _lastId; /// <param name="min"></param>
/// <param name="max"></param>
/// <param name="value"></param>
/// <returns></returns>
public static decimal ClampValue(decimal min, decimal max, decimal value)
{
value = Math.Min(max, value);
value = Math.Max(min, value);
return value;
}
/// <summary> /// <summary>
/// Clamp a value between a min and max /// Adjust a value to be between the min and max parameters and rounded to the closest step.
/// </summary> /// </summary>
/// <param name="min"></param> /// <param name="min">The min value</param>
/// <param name="max"></param> /// <param name="max">The max value</param>
/// <param name="value"></param> /// <param name="step">The step size the value should be floored to. For example, value 2.548 with a step size of 0.01 will output 2.54</param>
/// <returns></returns> /// <param name="roundingType">How to round</param>
public static decimal ClampValue(decimal min, decimal max, decimal value) /// <param name="value">The input value</param>
{ /// <returns></returns>
value = Math.Min(max, value); public static decimal AdjustValueStep(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal value)
value = Math.Max(min, value); {
if(step == 0)
throw new ArgumentException($"0 not allowed for parameter {nameof(step)}, pass in null to ignore the step size", nameof(step));
value = Math.Min(max, value);
value = Math.Max(min, value);
if (step == null)
return value; return value;
}
/// <summary> var offset = value % step.Value;
/// Adjust a value to be between the min and max parameters and rounded to the closest step. if(roundingType == RoundingType.Down)
/// </summary>
/// <param name="min">The min value</param>
/// <param name="max">The max value</param>
/// <param name="step">The step size the value should be floored to. For example, value 2.548 with a step size of 0.01 will output 2.54</param>
/// <param name="roundingType">How to round</param>
/// <param name="value">The input value</param>
/// <returns></returns>
public static decimal AdjustValueStep(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal value)
{ {
if(step == 0) value -= offset;
throw new ArgumentException($"0 not allowed for parameter {nameof(step)}, pass in null to ignore the step size", nameof(step)); }
else if(roundingType == RoundingType.Up)
value = Math.Min(max, value); {
value = Math.Max(min, value); if (offset != 0)
if (step == null) value += (step.Value - offset);
return value; }
else
var offset = value % step.Value; {
if(roundingType == RoundingType.Down) if (offset < step / 2)
{
value -= offset; value -= offset;
} else value += (step.Value - offset);
else if(roundingType == RoundingType.Up)
{
if (offset != 0)
value += (step.Value - offset);
}
else
{
if (offset < step / 2)
value -= offset;
else value += (step.Value - offset);
}
return value.Normalize();
} }
/// <summary> value = RoundDown(value, 8);
/// Adjust a value to be between the min and max parameters and rounded to the closest precision.
/// </summary>
/// <param name="min">The min value</param>
/// <param name="max">The max value</param>
/// <param name="precision">The precision the value should be rounded to. For example, value 2.554215 with a precision of 5 will output 2.5542</param>
/// <param name="roundingType">How to round</param>
/// <param name="value">The input value</param>
/// <returns></returns>
public static decimal AdjustValuePrecision(decimal min, decimal max, int? precision, RoundingType roundingType, decimal value)
{
value = Math.Min(max, value);
value = Math.Max(min, value);
if (precision == null)
return value;
return RoundToSignificantDigits(value, precision.Value, roundingType); return value.Normalize();
} }
/// <summary>
/// Apply the provided rules to the value
/// </summary>
/// <param name="value">Value to be adjusted</param>
/// <param name="decimals">Max decimal places</param>
/// <param name="valueStep">The value step for increase/decrease value</param>
/// <returns></returns>
public static decimal ApplyRules(
decimal value,
int? decimals = null,
decimal? valueStep = null)
{
if (valueStep.HasValue)
{
var offset = value % valueStep.Value;
if (offset != 0)
{
if (offset < valueStep.Value / 2)
value -= offset;
else value += (valueStep.Value - offset);
}
}
if (decimals.HasValue)
value = Math.Round(value, decimals.Value);
/// <summary>
/// Adjust a value to be between the min and max parameters and rounded to the closest precision.
/// </summary>
/// <param name="min">The min value</param>
/// <param name="max">The max value</param>
/// <param name="precision">The precision the value should be rounded to. For example, value 2.554215 with a precision of 5 will output 2.5542</param>
/// <param name="roundingType">How to round</param>
/// <param name="value">The input value</param>
/// <returns></returns>
public static decimal AdjustValuePrecision(decimal min, decimal max, int? precision, RoundingType roundingType, decimal value)
{
value = Math.Min(max, value);
value = Math.Max(min, value);
if (precision == null)
return value; return value;
}
/// <summary> return RoundToSignificantDigits(value, precision.Value, roundingType);
/// Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12 }
/// </summary>
/// <param name="value">The value to round</param> /// <summary>
/// <param name="digits">The total amount of digits (NOT decimal places) to round to</param> /// Apply the provided rules to the value
/// <param name="roundingType">How to round</param> /// </summary>
/// <returns></returns> /// <param name="value">Value to be adjusted</param>
public static decimal RoundToSignificantDigits(decimal value, int digits, RoundingType roundingType) /// <param name="decimals">Max decimal places</param>
/// <param name="valueStep">The value step for increase/decrease value</param>
/// <returns></returns>
public static decimal ApplyRules(
decimal value,
int? decimals = null,
decimal? valueStep = null)
{
if (valueStep.HasValue)
{ {
var val = (double)value; var offset = value % valueStep.Value;
if (value == 0) if (offset != 0)
return 0; {
if (offset < valueStep.Value / 2)
double scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(val))) + 1); value -= offset;
if(roundingType == RoundingType.Closest) else value += (valueStep.Value - offset);
return (decimal)(scale * Math.Round(val / scale, digits)); }
else
return (decimal)(scale * (double)RoundDown((decimal)(val / scale), digits));
} }
if (decimals.HasValue)
value = Math.Round(value, decimals.Value);
/// <summary> return value;
/// Rounds a value down }
/// </summary>
public static decimal RoundDown(decimal i, double decimalPlaces)
{
var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces));
return Math.Floor(i * power) / power;
}
/// <summary> /// <summary>
/// Rounds a value up /// Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12
/// </summary> /// </summary>
public static decimal RoundUp(decimal i, double decimalPlaces) /// <param name="value">The value to round</param>
{ /// <param name="digits">The total amount of digits (NOT decimal places) to round to</param>
var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces)); /// <param name="roundingType">How to round</param>
return Math.Ceiling(i * power) / power; /// <returns></returns>
} public static decimal RoundToSignificantDigits(decimal value, int digits, RoundingType roundingType)
{
var val = (double)value;
if (value == 0)
return 0;
/// <summary> double scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(val))) + 1);
/// Strips any trailing zero's of a decimal value, useful when converting the value to string. if(roundingType == RoundingType.Closest)
/// </summary> return (decimal)(scale * Math.Round(val / scale, digits));
/// <param name="value"></param> else
/// <returns></returns> return (decimal)(scale * (double)RoundDown((decimal)(val / scale), digits));
public static decimal Normalize(this decimal value) }
{
return value / 1.000000000000000000000000000000000m;
}
/// <summary> /// <summary>
/// Generate a new unique id. The id is statically stored so it is guaranteed to be unique /// Rounds a value down
/// </summary> /// </summary>
/// <returns></returns> public static decimal RoundDown(decimal i, double decimalPlaces)
public static int NextId() => Interlocked.Increment(ref _lastId); {
var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces));
return Math.Floor(i * power) / power;
}
/// <summary> /// <summary>
/// Return the last unique id that was generated /// Rounds a value up
/// </summary> /// </summary>
/// <returns></returns> public static decimal RoundUp(decimal i, double decimalPlaces)
public static int LastId() => _lastId; {
var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces));
return Math.Ceiling(i * power) / power;
}
/// <summary> /// <summary>
/// Generate a random string of specified length /// Strips any trailing zero's of a decimal value, useful when converting the value to string.
/// </summary> /// </summary>
/// <param name="length">Length of the random string</param> /// <param name="value"></param>
/// <returns></returns> /// <returns></returns>
public static string RandomString(int length) public static decimal Normalize(this decimal value)
{ {
var randomChars = new char[length]; return value / 1.000000000000000000000000000000000m;
}
/// <summary>
/// Generate a new unique id. The id is statically stored so it is guaranteed to be unique
/// </summary>
/// <returns></returns>
public static int NextId() => Interlocked.Increment(ref _lastId);
/// <summary>
/// Return the last unique id that was generated
/// </summary>
/// <returns></returns>
public static int LastId() => _lastId;
/// <summary>
/// Generate a random string of specified length
/// </summary>
/// <param name="length">Length of the random string</param>
/// <returns></returns>
public static string RandomString(int length)
{
var randomChars = new char[length];
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER #if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
for (int i = 0; i < length; i++) for (int i = 0; i < length; i++)
randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)]; randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)];
#else #else
var random = new Random(); var random = new Random();
for (int i = 0; i < length; i++) for (int i = 0; i < length; i++)
randomChars[i] = _allowedRandomChars[random.Next(0, _allowedRandomChars.Length)]; randomChars[i] = _allowedRandomChars[random.Next(0, _allowedRandomChars.Length)];
#endif #endif
return new string(randomChars); return new string(randomChars);
} }
/// <summary> /// <summary>
/// Generate a random string of specified length /// Generate a random string of specified length
/// </summary> /// </summary>
/// <param name="length">Length of the random string</param> /// <param name="length">Length of the random string</param>
/// <returns></returns> /// <returns></returns>
public static string RandomHexString(int length) public static string RandomHexString(int length)
{ {
#if NET9_0_OR_GREATER #if NET9_0_OR_GREATER
return "0x" + RandomNumberGenerator.GetHexString(length * 2); return "0x" + RandomNumberGenerator.GetHexString(length * 2);
#else #else
var randomChars = new char[length * 2]; var randomChars = new char[length * 2];
var random = new Random(); var random = new Random();
for (int i = 0; i < length * 2; i++) for (int i = 0; i < length * 2; i++)
randomChars[i] = _allowedRandomHexChars[random.Next(0, _allowedRandomHexChars.Length)]; randomChars[i] = _allowedRandomHexChars[random.Next(0, _allowedRandomHexChars.Length)];
return "0x" + new string(randomChars); return "0x" + new string(randomChars);
#endif #endif
} }
/// <summary> /// <summary>
/// Generate a long value /// Generate a long value
/// </summary> /// </summary>
/// <param name="maxLength">Max number of digits</param> /// <param name="maxLength">Max character length</param>
public static long RandomLong(int maxLength) /// <returns></returns>
{ public static long RandomLong(int maxLength)
{
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER #if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
var value = RandomNumberGenerator.GetInt32(0, int.MaxValue); var value = RandomNumberGenerator.GetInt32(0, int.MaxValue);
#else #else
var random = new Random(); var random = new Random();
var value = random.Next(0, int.MaxValue); var value = random.Next(0, int.MaxValue);
#endif #endif
var val = value.ToString(); var val = value.ToString();
if (val.Length > maxLength) if (val.Length > maxLength)
return int.Parse(val.Substring(0, maxLength)); return int.Parse(val.Substring(0, maxLength));
else else
return value; return value;
} }
/// <summary> /// <summary>
/// Generate a long value between two values /// Generate a random string of specified length
/// </summary> /// </summary>
/// <param name="minValue">Min value</param> /// <param name="source">The initial string</param>
/// <param name="maxValue">Max value</param> /// <param name="totalLength">Total length of the resulting string</param>
/// <returns></returns> /// <returns></returns>
public static long RandomLong(long minValue, long maxValue) public static string AppendRandomString(string source, int totalLength)
{
if (totalLength < source.Length)
throw new ArgumentException("Total length smaller than source string length", nameof(totalLength));
if (totalLength == source.Length)
return source;
return source + RandomString(totalLength - source.Length);
}
/// <summary>
/// Get the month representation for futures symbol based on the delivery month
/// </summary>
/// <param name="time">Delivery time</param>
/// <returns></returns>
public static string GetDeliveryMonthSymbol(DateTime time) => _monthSymbols[time.Month];
/// <summary>
/// Execute multiple requests to retrieve multiple pages of the result set
/// </summary>
/// <typeparam name="TResult">Type of the client</typeparam>
/// <typeparam name="TRequest">Type of the request</typeparam>
/// <param name="paginatedFunc">The func to execute with each request</param>
/// <param name="request">The request parameters</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
public static async IAsyncEnumerable<ExchangeWebResult<TResult[]>> ExecutePages<TResult, TRequest>(Func<TRequest, INextPageToken?, CancellationToken, Task<ExchangeWebResult<TResult[]>>> paginatedFunc, TRequest request, [EnumeratorCancellation]CancellationToken ct = default)
{
var result = new List<TResult>();
ExchangeWebResult<TResult[]> batch;
INextPageToken? nextPageToken = null;
while (true)
{ {
#if NET8_0_OR_GREATER batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
var buf = RandomNumberGenerator.GetBytes(8); yield return batch;
#else if (!batch || ct.IsCancellationRequested)
byte[] buf = new byte[8]; break;
var random = new Random();
random.NextBytes(buf);
#endif
long longRand = BitConverter.ToInt64(buf, 0);
return (Math.Abs(longRand % (maxValue - minValue)) + minValue);
}
/// <summary> result.AddRange(batch.Data);
/// Generate a random string of specified length nextPageToken = batch.NextPageToken;
/// </summary> if (nextPageToken == null)
/// <param name="source">The initial string</param> break;
/// <param name="totalLength">Total length of the resulting string</param>
/// <returns></returns>
public static string AppendRandomString(string source, int totalLength)
{
if (totalLength < source.Length)
throw new ArgumentException("Total length smaller than source string length", nameof(totalLength));
if (totalLength == source.Length)
return source;
return source + RandomString(totalLength - source.Length);
}
/// <summary>
/// Get the month representation for futures symbol based on the delivery month
/// </summary>
/// <param name="time">Delivery time</param>
/// <returns></returns>
public static string GetDeliveryMonthSymbol(DateTime time) => _monthSymbols[time.Month];
/// <summary>
/// Execute multiple requests to retrieve multiple pages of the result set
/// </summary>
/// <typeparam name="T">Type of the client</typeparam>
/// <typeparam name="U">Type of the request</typeparam>
/// <param name="paginatedFunc">The func to execute with each request</param>
/// <param name="request">The request parameters</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
public static async IAsyncEnumerable<ExchangeWebResult<T[]>> ExecutePages<T, U>(Func<U, INextPageToken?, CancellationToken, Task<ExchangeWebResult<T[]>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
{
var result = new List<T>();
ExchangeWebResult<T[]> batch;
INextPageToken? nextPageToken = null;
while (true)
{
batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
yield return batch;
if (!batch || ct.IsCancellationRequested)
break;
result.AddRange(batch.Data);
nextPageToken = batch.NextPageToken;
if (nextPageToken == null)
break;
}
}
/// <summary>
/// Apply the rules (price and quantity step size and decimals precision, min/max quantity) from the symbol to the quantity and price
/// </summary>
/// <param name="symbol">The symbol as retrieved from the exchange</param>
/// <param name="quantity">Quantity to trade</param>
/// <param name="price">Price to trade at</param>
/// <param name="adjustedQuantity">Quantity adjusted to match all trading rules</param>
/// <param name="adjustedPrice">Price adjusted to match all trading rules</param>
public static void ApplySymbolRules(SharedSpotSymbol symbol, decimal quantity, decimal? price, out decimal adjustedQuantity, out decimal? adjustedPrice)
{
adjustedPrice = price;
adjustedQuantity = quantity;
var minNotionalAdjust = false;
if (price != null)
{
adjustedPrice = AdjustValueStep(0, decimal.MaxValue, symbol.PriceStep, RoundingType.Down, price.Value);
adjustedPrice = symbol.PriceSignificantFigures.HasValue ? RoundToSignificantDigits(adjustedPrice.Value, symbol.PriceSignificantFigures.Value, RoundingType.Closest) : adjustedPrice;
adjustedPrice = symbol.PriceDecimals.HasValue ? RoundDown(price.Value, symbol.PriceDecimals.Value) : adjustedPrice;
if (adjustedPrice != 0 && adjustedPrice * quantity < symbol.MinNotionalValue)
{
adjustedQuantity = symbol.MinNotionalValue.Value / adjustedPrice.Value;
minNotionalAdjust = true;
}
}
adjustedQuantity = AdjustValueStep(symbol.MinTradeQuantity ?? 0, symbol.MaxTradeQuantity ?? decimal.MaxValue, symbol.QuantityStep, minNotionalAdjust ? RoundingType.Up : RoundingType.Down, adjustedQuantity);
adjustedQuantity = symbol.QuantityDecimals.HasValue ? (minNotionalAdjust ? RoundUp(adjustedQuantity, symbol.QuantityDecimals.Value) : RoundDown(adjustedQuantity, symbol.QuantityDecimals.Value)) : adjustedQuantity;
}
/// <summary>
/// Queue updates received from a websocket subscriptions and process them async
/// </summary>
/// <typeparam name="T">The queued update type</typeparam>
/// <param name="subscribeCall">The subscribe call</param>
/// <param name="asyncHandler">The async update handler</param>
/// <param name="maxQueuedItems">The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with <see>fullMode</see></param>
/// <param name="fullBehavior">What should happen if the queue contains <see>maxQueuedItems</see> pending updates. If no max is set this setting is ignored</param>
public static async Task<CallResult<UpdateSubscription>> ProcessQueuedAsync<T>(
Func<Action<DataEvent<T>>, Task<CallResult<UpdateSubscription>>> subscribeCall,
Func<DataEvent<T>, Task> asyncHandler,
int? maxQueuedItems = null,
QueueFullBehavior? fullBehavior = null)
{
var processor = new ProcessQueue<DataEvent<T>>(asyncHandler, maxQueuedItems, fullBehavior);
await processor.StartAsync().ConfigureAwait(false);
var result = await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false);
if (!result)
{
await processor.StopAsync().ConfigureAwait(false);
return result;
}
processor.Exception += result.Data._subscription.InvokeExceptionHandler;
result.Data.SubscriptionStatusChanged += (upd) =>
{
if (upd == CryptoExchange.Net.Objects.SubscriptionStatus.Closed)
_ = processor.StopAsync(true);
};
return result;
}
/// <summary>
/// Queue updates and process them async
/// </summary>
/// <typeparam name="T">The queued update type</typeparam>
/// <param name="subscribeCall">The subscribe call</param>
/// <param name="asyncHandler">The async update handler</param>
/// <param name="maxQueuedItems">The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with <see>fullMode</see></param>
/// <param name="fullBehavior">What should happen if the queue contains <see>maxQueuedItems</see> pending updates. If no max is set this setting is ignored</param>
/// <param name="ct">Cancellation token to stop the processing</param>
public static async Task ProcessQueuedAsync<T>(
Func<Action<T>, Task> subscribeCall,
Func<T, Task> asyncHandler,
CancellationToken ct,
int? maxQueuedItems = null,
QueueFullBehavior? fullBehavior = null)
{
var processor = new ProcessQueue<T>(asyncHandler, maxQueuedItems, fullBehavior);
await processor.StartAsync().ConfigureAwait(false);
ct.Register(async () =>
{
await processor.StopAsync().ConfigureAwait(false);
});
await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false);
}
/// <summary>
/// Queue updates received from a websocket subscriptions and process them async
/// </summary>
/// <typeparam name="TEventType">The type of the queued item</typeparam>
/// <typeparam name="TOutputType">The type of the item to pass to the processor</typeparam>
/// <param name="subscribeCall">The subscribe call</param>
/// <param name="mapper">The mapper function to go from <see>TEventType</see> to <see>TOutputType</see></param>
/// <param name="asyncHandler">The async update handler</param>
/// <param name="maxQueuedItems">The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with <see>fullMode</see></param>
/// <param name="fullBehavior">What should happen if the queue contains <see>maxQueuedItems</see> pending updates. If no max is set this setting is ignored</param>
public static async Task<CallResult<UpdateSubscription>> ProcessQueuedAsync<TEventType, TOutputType>(
Func<ProcessQueue<DataEvent<TEventType>>, Task<CallResult<UpdateSubscription>>> subscribeCall,
Func<DataEvent<TEventType>, DataEvent<TOutputType>> mapper,
Func<DataEvent<TOutputType>, Task> asyncHandler,
int? maxQueuedItems = null,
QueueFullBehavior? fullBehavior = null
)
{
var processor = new ProcessQueue<DataEvent<TEventType>>((update) => {
return asyncHandler.Invoke(mapper.Invoke(update));
}, maxQueuedItems, fullBehavior);
await processor.StartAsync().ConfigureAwait(false);
var result = await subscribeCall(processor).ConfigureAwait(false);
if (!result)
{
await processor.StopAsync().ConfigureAwait(false);
return result;
}
processor.Exception += result.Data._subscription.InvokeExceptionHandler;
result.Data.SubscriptionStatusChanged += (upd) =>
{
if (upd == SubscriptionStatus.Closed)
_ = processor.StopAsync(true);
};
return result;
}
/// <summary>
/// Parse a decimal value from a string
/// </summary>
public static decimal? ParseDecimal(string? value)
{
// Value is null or empty is the most common case to return null so check before trying to parse
if (string.IsNullOrEmpty(value))
return null;
// Try parse, only fails for these reasons:
// 1. string is null or empty
// 2. value is larger or smaller than decimal max/min
// 3. unparsable format
if (decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var decValue))
return decValue;
// Check for values which should be parsed to null
if (string.Equals("null", value, StringComparison.OrdinalIgnoreCase)
|| string.Equals("NaN", value, StringComparison.OrdinalIgnoreCase))
{
return null;
}
// Infinity value should be parsed to min/max value
if (string.Equals("Infinity", value, StringComparison.OrdinalIgnoreCase))
return decimal.MaxValue;
else if(string.Equals("-Infinity", value, StringComparison.OrdinalIgnoreCase))
return decimal.MinValue;
if (value!.Length > 27 && decimal.TryParse(value.Substring(0, 27), out var overflowValue))
{
// Not a valid decimal value and more than 27 chars, from which the first part can be parsed correctly.
// assume overflow
if (overflowValue < 0)
return decimal.MinValue;
else
return decimal.MaxValue;
}
// Unknown decimal format, return null
return null;
} }
} }
/// <summary>
/// Apply the rules (price and quantity step size and decimals precision, min/max quantity) from the symbol to the quantity and price
/// </summary>
/// <param name="symbol">The symbol as retrieved from the exchange</param>
/// <param name="quantity">Quantity to trade</param>
/// <param name="price">Price to trade at</param>
/// <param name="adjustedQuantity">Quantity adjusted to match all trading rules</param>
/// <param name="adjustedPrice">Price adjusted to match all trading rules</param>
public static void ApplySymbolRules(SharedSpotSymbol symbol, decimal quantity, decimal? price, out decimal adjustedQuantity, out decimal? adjustedPrice)
{
adjustedPrice = price;
adjustedQuantity = quantity;
var minNotionalAdjust = false;
if (price != null)
{
adjustedPrice = AdjustValueStep(0, decimal.MaxValue, symbol.PriceStep, RoundingType.Down, price.Value);
adjustedPrice = symbol.PriceSignificantFigures.HasValue ? RoundToSignificantDigits(adjustedPrice.Value, symbol.PriceSignificantFigures.Value, RoundingType.Closest) : adjustedPrice;
adjustedPrice = symbol.PriceDecimals.HasValue ? RoundDown(price.Value, symbol.PriceDecimals.Value) : adjustedPrice;
if (adjustedPrice != 0 && adjustedPrice * quantity < symbol.MinNotionalValue)
{
adjustedQuantity = symbol.MinNotionalValue.Value / adjustedPrice.Value;
minNotionalAdjust = true;
}
}
adjustedQuantity = AdjustValueStep(symbol.MinTradeQuantity ?? 0, symbol.MaxTradeQuantity ?? decimal.MaxValue, symbol.QuantityStep, minNotionalAdjust ? RoundingType.Up : RoundingType.Down, adjustedQuantity);
adjustedQuantity = symbol.QuantityDecimals.HasValue ? (minNotionalAdjust ? RoundUp(adjustedQuantity, symbol.QuantityDecimals.Value) : RoundDown(adjustedQuantity, symbol.QuantityDecimals.Value)) : adjustedQuantity;
}
/// <summary>
/// Parse a decimal value from a string
/// </summary>
public static decimal? ParseDecimal(string? value)
{
// Value is null or empty is the most common case to return null so check before trying to parse
if (string.IsNullOrEmpty(value))
return null;
// Try parse, only fails for these reasons:
// 1. string is null or empty
// 2. value is larger or smaller than decimal max/min
// 3. unparsable format
if (decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var decValue))
return decValue;
// Check for values which should be parsed to null
if (string.Equals("null", value, StringComparison.OrdinalIgnoreCase)
|| string.Equals("NaN", value, StringComparison.OrdinalIgnoreCase))
{
return null;
}
// Infinity value should be parsed to min/max value
if (string.Equals("Infinity", value, StringComparison.OrdinalIgnoreCase))
return decimal.MaxValue;
else if(string.Equals("-Infinity", value, StringComparison.OrdinalIgnoreCase))
return decimal.MinValue;
if (value!.Length > 27 && decimal.TryParse(value.Substring(0, 27), out var overflowValue))
{
// Not a valid decimal value and more than 27 chars, from which the first part can be parsed correctly.
// assume overflow
if (overflowValue < 0)
return decimal.MinValue;
else
return decimal.MaxValue;
}
// Unknown decimal format, return null
return null;
}
} }
+49 -50
View File
@@ -1,69 +1,68 @@
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace CryptoExchange.Net namespace CryptoExchange.Net;
/// <summary>
/// Cache for symbol parsing
/// </summary>
public static class ExchangeSymbolCache
{ {
private static ConcurrentDictionary<string, ExchangeInfo> _symbolInfos = new ConcurrentDictionary<string, ExchangeInfo>();
/// <summary> /// <summary>
/// Cache for symbol parsing /// Update the cached symbol data for an exchange
/// </summary> /// </summary>
public static class ExchangeSymbolCache /// <param name="topicId">Id for the provided data</param>
/// <param name="updateData">Symbol data</param>
public static void UpdateSymbolInfo(string topicId, SharedSpotSymbol[] updateData)
{ {
private static ConcurrentDictionary<string, ExchangeInfo> _symbolInfos = new ConcurrentDictionary<string, ExchangeInfo>(); if(!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
/// <summary>
/// Update the cached symbol data for an exchange
/// </summary>
/// <param name="topicId">Id for the provided data</param>
/// <param name="updateData">Symbol data</param>
public static void UpdateSymbolInfo(string topicId, SharedSpotSymbol[] updateData)
{ {
if(!_symbolInfos.TryGetValue(topicId, out var exchangeInfo)) exchangeInfo = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
{ _symbolInfos.TryAdd(topicId, exchangeInfo);
exchangeInfo = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
_symbolInfos.TryAdd(topicId, exchangeInfo);
}
if (DateTime.UtcNow - exchangeInfo.UpdateTime < TimeSpan.FromMinutes(60))
return;
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
} }
/// <summary> if (DateTime.UtcNow - exchangeInfo.UpdateTime < TimeSpan.FromMinutes(60))
/// Parse a symbol name to a SharedSymbol return;
/// </summary>
/// <param name="topicId">Id for the provided data</param> _symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
/// <param name="symbolName">Symbol name</param> }
public static SharedSymbol? ParseSymbol(string topicId, string? symbolName)
/// <summary>
/// Parse a symbol name to a SharedSymbol
/// </summary>
/// <param name="topicId">Id for the provided data</param>
/// <param name="symbolName">Symbol name</param>
public static SharedSymbol? ParseSymbol(string topicId, string? symbolName)
{
if (symbolName == null)
return null;
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
return null;
if (!exchangeInfo.Symbols.TryGetValue(symbolName, out var symbolInfo))
return null;
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
{ {
if (symbolName == null) DeliverTime = symbolInfo.DeliverTime
return null; };
}
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo)) class ExchangeInfo
return null; {
public DateTime UpdateTime { get; set; }
public Dictionary<string, SharedSymbol> Symbols { get; set; }
if (!exchangeInfo.Symbols.TryGetValue(symbolName, out var symbolInfo)) public ExchangeInfo(DateTime updateTime, Dictionary<string, SharedSymbol> symbols)
return null;
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
{
DeliverTime = symbolInfo.DeliverTime
};
}
class ExchangeInfo
{ {
public DateTime UpdateTime { get; set; } UpdateTime = updateTime;
public Dictionary<string, SharedSymbol> Symbols { get; set; } Symbols = symbols;
public ExchangeInfo(DateTime updateTime, Dictionary<string, SharedSymbol> symbols)
{
UpdateTime = updateTime;
Symbols = symbols;
}
} }
} }
} }
File diff suppressed because it is too large Load Diff
@@ -1,47 +0,0 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using System;
namespace CryptoExchange.Net.Interfaces.Clients
{
/// <summary>
/// Base api client
/// </summary>
public interface IBaseApiClient
{
/// <summary>
/// Base address
/// </summary>
string BaseAddress { get; }
/// <summary>
/// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid.
/// </summary>
bool Authenticated { get; }
/// <summary>
/// Format a base and quote asset to an exchange accepted symbol
/// </summary>
/// <param name="baseAsset">The base asset</param>
/// <param name="quoteAsset">The quote asset</param>
/// <param name="tradingMode">The trading mode</param>
/// <param name="deliverDate">The deliver date for a delivery futures symbol</param>
/// <returns></returns>
string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
/// <summary>
/// Set the API credentials for this API client
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="credentials"></param>
void SetApiCredentials<T>(T credentials) where T : ApiCredentials;
/// <summary>
/// Set new options. Note that when using a proxy this should be provided in the options even when already set before or it will be reset.
/// </summary>
/// <typeparam name="T">Api credentials type</typeparam>
/// <param name="options">Options to set</param>
void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials;
}
}
@@ -1,17 +0,0 @@
using System;
namespace CryptoExchange.Net.Interfaces.Clients
{
/// <summary>
/// Client for accessing REST API's for different exchanges
/// </summary>
public interface ICryptoRestClient
{
/// <summary>
/// Try get
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
T TryGet<T>(Func<T> createFunc);
}
}
@@ -1,17 +0,0 @@
using System;
namespace CryptoExchange.Net.Interfaces.Clients
{
/// <summary>
/// Client for accessing Websocket API's for different exchanges
/// </summary>
public interface ICryptoSocketClient
{
/// <summary>
/// Try get a client by type for the service collection
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
T TryGet<T>(Func<T> createFunc);
}
}
@@ -1,18 +0,0 @@
namespace CryptoExchange.Net.Interfaces.Clients
{
/// <summary>
/// Base rest API client
/// </summary>
public interface IRestApiClient : IBaseApiClient
{
/// <summary>
/// The factory for creating requests. Used for unit testing
/// </summary>
IRequestFactory RequestFactory { get; set; }
/// <summary>
/// Total amount of requests made with this API client
/// </summary>
int TotalRequestsMade { get; set; }
}
}
@@ -1,26 +0,0 @@
using System;
using CryptoExchange.Net.Objects.Options;
namespace CryptoExchange.Net.Interfaces.Clients
{
/// <summary>
/// Base class for rest API implementations
/// </summary>
public interface IRestClient: IDisposable
{
/// <summary>
/// The options provided for this client
/// </summary>
ExchangeOptions ClientOptions { get; }
/// <summary>
/// The total amount of requests made with this client
/// </summary>
int TotalRequestsMade { get; }
/// <summary>
/// The exchange name
/// </summary>
string Exchange { get; }
}
}
@@ -1,76 +0,0 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets.Default.Interfaces;
using CryptoExchange.Net.Sockets.HighPerf.Interfaces;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces.Clients
{
/// <summary>
/// Socket API client
/// </summary>
public interface ISocketApiClient: IBaseApiClient
{
/// <summary>
/// The current amount of socket connections on the API client
/// </summary>
int CurrentConnections { get; }
/// <summary>
/// The current amount of subscriptions over all connections
/// </summary>
int CurrentSubscriptions { get; }
/// <summary>
/// Incoming data Kbps
/// </summary>
double IncomingKbps { get; }
/// <summary>
/// The factory for creating sockets. Used for unit testing
/// </summary>
IWebsocketFactory SocketFactory { get; set; }
/// <summary>
/// High performance websocket factory
/// </summary>
IHighPerfConnectionFactory? HighPerfConnectionFactory { get; set; }
/// <summary>
/// Current client options
/// </summary>
SocketExchangeOptions ClientOptions { get; }
/// <summary>
/// Current API options
/// </summary>
SocketApiOptions ApiOptions { get; }
/// <summary>
/// Log the current state of connections and subscriptions
/// </summary>
string GetSubscriptionsState(bool includeSubDetails = true);
/// <summary>
/// Reconnect all connections
/// </summary>
/// <returns></returns>
Task ReconnectAsync();
/// <summary>
/// Unsubscribe all subscriptions
/// </summary>
/// <returns></returns>
Task UnsubscribeAllAsync();
/// <summary>
/// Unsubscribe an update subscription
/// </summary>
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
/// <returns></returns>
Task<bool> UnsubscribeAsync(int subscriptionId);
/// <summary>
/// Unsubscribe an update subscription
/// </summary>
/// <param name="subscription">The subscription to unsubscribe</param>
/// <returns></returns>
Task UnsubscribeAsync(UpdateSubscription subscription);
/// <summary>
/// Prepare connections which can subsequently be used for sending websocket requests. Note that this is not required. If not prepared it will be initialized at the first websocket request.
/// </summary>
/// <returns></returns>
Task<CallResult> PrepareConnectionsAsync();
}
}
@@ -1,58 +0,0 @@
using System;
using System.Threading.Tasks;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
namespace CryptoExchange.Net.Interfaces.Clients
{
/// <summary>
/// Base class for socket API implementations
/// </summary>
public interface ISocketClient: IDisposable
{
/// <summary>
/// The exchange name
/// </summary>
string Exchange { get; }
/// <summary>
/// The options provided for this client
/// </summary>
ExchangeOptions ClientOptions { get; }
/// <summary>
/// Incoming kilobytes per second of data
/// </summary>
public double IncomingKbps { get; }
/// <summary>
/// The current amount of connections to the API from this client. A connection can have multiple subscriptions.
/// </summary>
public int CurrentConnections { get; }
/// <summary>
/// The current amount of subscriptions running from the client
/// </summary>
public int CurrentSubscriptions { get; }
/// <summary>
/// Unsubscribe from a stream using the subscription id received when starting the subscription
/// </summary>
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
/// <returns></returns>
Task UnsubscribeAsync(int subscriptionId);
/// <summary>
/// Unsubscribe from a stream
/// </summary>
/// <param name="subscription">The subscription to unsubscribe</param>
/// <returns></returns>
Task UnsubscribeAsync(UpdateSubscription subscription);
/// <summary>
/// Unsubscribe all subscriptions
/// </summary>
/// <returns></returns>
Task UnsubscribeAllAsync();
}
}
@@ -1,16 +1,15 @@
using System; using System;
namespace CryptoExchange.Net.Interfaces namespace CryptoExchange.Net.Interfaces;
/// <summary>
/// Time provider
/// </summary>
internal interface IAuthTimeProvider
{ {
/// <summary> /// <summary>
/// Time provider /// Get current time
/// </summary> /// </summary>
internal interface IAuthTimeProvider /// <returns></returns>
{ DateTime GetTime();
/// <summary>
/// Get current time
/// </summary>
/// <returns></returns>
DateTime GetTime();
}
} }
@@ -0,0 +1,46 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using System;
namespace CryptoExchange.Net.Interfaces;
/// <summary>
/// Base api client
/// </summary>
public interface IBaseApiClient
{
/// <summary>
/// Base address
/// </summary>
string BaseAddress { get; }
/// <summary>
/// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid.
/// </summary>
bool Authenticated { get; }
/// <summary>
/// Format a base and quote asset to an exchange accepted symbol
/// </summary>
/// <param name="baseAsset">The base asset</param>
/// <param name="quoteAsset">The quote asset</param>
/// <param name="tradingMode">The trading mode</param>
/// <param name="deliverDate">The deliver date for a delivery futures symbol</param>
/// <returns></returns>
string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
/// <summary>
/// Set the API credentials for this API client
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="credentials"></param>
void SetApiCredentials<T>(T credentials) where T : ApiCredentials;
/// <summary>
/// Set new options. Note that when using a proxy this should be provided in the options even when already set before or it will be reset.
/// </summary>
/// <typeparam name="T">Api credentials type</typeparam>
/// <param name="options">Options to set</param>
void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials;
}
@@ -0,0 +1,16 @@
using System;
namespace CryptoExchange.Net.Interfaces;
/// <summary>
/// Client for accessing REST API's for different exchanges
/// </summary>
public interface ICryptoRestClient
{
/// <summary>
/// Try get
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
T TryGet<T>(Func<T> createFunc);
}
@@ -0,0 +1,16 @@
using System;
namespace CryptoExchange.Net.Interfaces;
/// <summary>
/// Client for accessing Websocket API's for different exchanges
/// </summary>
public interface ICryptoSocketClient
{
/// <summary>
/// Try get a client by type for the service collection
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
T TryGet<T>(Func<T> createFunc);
}
@@ -1,13 +0,0 @@
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Service for an exchange
/// </summary>
public interface IExchangeService
{
/// <summary>
/// The exchange the service is for
/// </summary>
public string ExchangeName { get; }
}
}
@@ -1,109 +1,110 @@
using CryptoExchange.Net.Converters.MessageParsing; using CryptoExchange.Net.Converters.MessageParsing;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using System; using System;
#if NET5_0_OR_GREATER
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
#endif
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces namespace CryptoExchange.Net.Interfaces;
/// <summary>
/// Message accessor
/// </summary>
public interface IMessageAccessor
{ {
/// <summary> /// <summary>
/// Message accessor /// Is this a valid message
/// </summary> /// </summary>
public interface IMessageAccessor bool IsValid { get; }
{ /// <summary>
/// <summary> /// Is the original data available for retrieval
/// Is this a valid message /// </summary>
/// </summary> bool OriginalDataAvailable { get; }
bool IsValid { get; } /// <summary>
/// <summary> /// The underlying data object
/// Is the original data available for retrieval /// </summary>
/// </summary> object? Underlying { get; }
bool OriginalDataAvailable { get; } /// <summary>
/// <summary> /// Clear internal data structure
/// The underlying data object /// </summary>
/// </summary> void Clear();
object? Underlying { get; } /// <summary>
/// <summary> /// Get the type of node
/// Clear internal data structure /// </summary>
/// </summary> /// <returns></returns>
void Clear(); NodeType? GetNodeType();
/// <summary> /// <summary>
/// Get the type of node /// Get the type of node
/// </summary> /// </summary>
/// <returns></returns> /// <param name="path">Access path</param>
NodeType? GetNodeType(); /// <returns></returns>
/// <summary> NodeType? GetNodeType(MessagePath path);
/// Get the type of node /// <summary>
/// </summary> /// Get the value of a path
/// <param name="path">Access path</param> /// </summary>
/// <returns></returns> /// <typeparam name="T"></typeparam>
NodeType? GetNodeType(MessagePath path); /// <param name="path"></param>
/// <summary> /// <returns></returns>
/// Get the value of a path T? GetValue<T>(MessagePath path);
/// </summary> /// <summary>
/// <typeparam name="T"></typeparam> /// Get the values of an array
/// <param name="path"></param> /// </summary>
/// <returns></returns> /// <typeparam name="T"></typeparam>
T? GetValue<T>(MessagePath path); /// <param name="path"></param>
/// <summary> /// <returns></returns>
/// Get the values of an array T?[]? GetValues<T>(MessagePath path);
/// </summary> /// <summary>
/// <typeparam name="T"></typeparam> /// Deserialize the message into this type
/// <param name="path"></param> /// </summary>
/// <returns></returns> /// <param name="type"></param>
T?[]? GetValues<T>(MessagePath path); /// <param name="path"></param>
/// <summary> /// <returns></returns>
/// Deserialize the message into this type
/// </summary>
/// <param name="type"></param>
/// <param name="path"></param>
/// <returns></returns>
#if NET5_0_OR_GREATER #if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif #endif
CallResult<object> Deserialize(Type type, MessagePath? path = null); CallResult<object> Deserialize(Type type, MessagePath? path = null);
/// <summary> /// <summary>
/// Deserialize the message into this type /// Deserialize the message into this type
/// </summary> /// </summary>
/// <param name="path"></param> /// <param name="path"></param>
/// <returns></returns> /// <returns></returns>
#if NET5_0_OR_GREATER #if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif #endif
CallResult<T> Deserialize<T>(MessagePath? path = null); CallResult<T> Deserialize<T>(MessagePath? path = null);
/// <summary>
/// Get the original string value
/// </summary>
/// <returns></returns>
string GetOriginalString();
}
/// <summary> /// <summary>
/// Stream message accessor /// Get the original string value
/// </summary> /// </summary>
public interface IStreamMessageAccessor : IMessageAccessor /// <returns></returns>
{ string GetOriginalString();
/// <summary> }
/// Load a stream message
/// </summary> /// <summary>
/// <param name="stream"></param> /// Stream message accessor
/// <param name="bufferStream"></param> /// </summary>
Task<CallResult> Read(Stream stream, bool bufferStream); public interface IStreamMessageAccessor : IMessageAccessor
} {
/// <summary>
/// <summary> /// Load a stream message
/// Byte message accessor /// </summary>
/// </summary> /// <param name="stream"></param>
public interface IByteMessageAccessor : IMessageAccessor /// <param name="bufferStream"></param>
{ Task<CallResult> Read(Stream stream, bool bufferStream);
/// <summary> }
/// Load a data message
/// </summary> /// <summary>
/// <param name="data"></param> /// Byte message accessor
CallResult Read(ReadOnlyMemory<byte> data); /// </summary>
} public interface IByteMessageAccessor : IMessageAccessor
{
/// <summary>
/// Load a data message
/// </summary>
/// <param name="data"></param>
CallResult Read(ReadOnlyMemory<byte> data);
} }
@@ -0,0 +1,33 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using System;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces;
/// <summary>
/// Message processor
/// </summary>
public interface IMessageProcessor
{
/// <summary>
/// Id of the processor
/// </summary>
public int Id { get; }
/// <summary>
/// The matcher for this listener
/// </summary>
public MessageMatcher MessageMatcher { get; }
/// <summary>
/// Handle a message
/// </summary>
Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message, MessageHandlerLink matchedHandler);
/// <summary>
/// Deserialize a message into object of type
/// </summary>
/// <param name="accessor"></param>
/// <param name="type"></param>
/// <returns></returns>
CallResult<object> Deserialize(IMessageAccessor accessor, Type type);
}
@@ -1,35 +1,34 @@
namespace CryptoExchange.Net.Interfaces namespace CryptoExchange.Net.Interfaces;
/// <summary>
/// Serializer interface
/// </summary>
public interface IMessageSerializer
{
}
/// <summary>
/// Serialize to byte array
/// </summary>
public interface IByteMessageSerializer: IMessageSerializer
{ {
/// <summary> /// <summary>
/// Serializer interface /// Serialize an object to a string
/// </summary> /// </summary>
public interface IMessageSerializer /// <param name="message"></param>
{ /// <returns></returns>
} byte[] Serialize<T>(T message);
}
/// <summary>
/// Serialize to byte array /// <summary>
/// </summary> /// Serialize to string
public interface IByteMessageSerializer: IMessageSerializer /// </summary>
{ public interface IStringMessageSerializer: IMessageSerializer
/// <summary> {
/// Serialize an object to a string /// <summary>
/// </summary> /// Serialize an object to a string
/// <param name="message"></param> /// </summary>
/// <returns></returns> /// <param name="message"></param>
byte[] Serialize<T>(T message); /// <returns></returns>
} string Serialize<T>(T message);
/// <summary>
/// Serialize to string
/// </summary>
public interface IStringMessageSerializer: IMessageSerializer
{
/// <summary>
/// Serialize an object to a string
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
string Serialize<T>(T message);
}
} }

Some files were not shown because too many files have changed in this diff Show More