diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index ff8e315..e15e9b0 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 9.0.x + dotnet-version: 10.0.x - name: Restore dependencies run: dotnet restore - name: Build diff --git a/CryptoExchange.Net.Protobuf/CryptoExchange.Net.Protobuf.csproj b/CryptoExchange.Net.Protobuf/CryptoExchange.Net.Protobuf.csproj index 41b82fe..f1f0d77 100644 --- a/CryptoExchange.Net.Protobuf/CryptoExchange.Net.Protobuf.csproj +++ b/CryptoExchange.Net.Protobuf/CryptoExchange.Net.Protobuf.csproj @@ -1,6 +1,6 @@ - + - netstandard2.0;netstandard2.1;net8.0;net9.0 + netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0 CryptoExchange.Net.Protobuf diff --git a/CryptoExchange.Net.UnitTests/AsyncResetEventTests.cs b/CryptoExchange.Net.UnitTests/AsyncResetEventTests.cs index 992d2df..78bab0f 100644 --- a/CryptoExchange.Net.UnitTests/AsyncResetEventTests.cs +++ b/CryptoExchange.Net.UnitTests/AsyncResetEventTests.cs @@ -4,7 +4,6 @@ using NUnit.Framework.Legacy; using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; namespace CryptoExchange.Net.UnitTests diff --git a/CryptoExchange.Net.UnitTests/BaseClientTests.cs b/CryptoExchange.Net.UnitTests/BaseClientTests.cs index f64b00e..aae4a95 100644 --- a/CryptoExchange.Net.UnitTests/BaseClientTests.cs +++ b/CryptoExchange.Net.UnitTests/BaseClientTests.cs @@ -1,11 +1,5 @@ -using CryptoExchange.Net.Authentication; -using CryptoExchange.Net.Objects; -using CryptoExchange.Net.UnitTests.TestImplementations; -using Microsoft.Extensions.Logging; -using NUnit.Framework; +using NUnit.Framework; using NUnit.Framework.Legacy; -using System; -using System.Collections.Generic; namespace CryptoExchange.Net.UnitTests { diff --git a/CryptoExchange.Net.UnitTests/CallResultTests.cs b/CryptoExchange.Net.UnitTests/CallResultTests.cs index 80c6532..b54bc48 100644 --- a/CryptoExchange.Net.UnitTests/CallResultTests.cs +++ b/CryptoExchange.Net.UnitTests/CallResultTests.cs @@ -4,11 +4,8 @@ using NUnit.Framework; using NUnit.Framework.Legacy; using System; using System.Collections.Generic; -using System.Linq; using System.Net; using System.Net.Http; -using System.Text; -using System.Threading.Tasks; namespace CryptoExchange.Net.UnitTests { @@ -115,7 +112,7 @@ namespace CryptoExchange.Net.UnitTests var result = new WebCallResult( System.Net.HttpStatusCode.OK, HttpVersion.Version11, - new KeyValuePair[0], + new HttpResponseMessage().Headers, TimeSpan.FromSeconds(1), null, "{}", @@ -123,7 +120,7 @@ namespace CryptoExchange.Net.UnitTests "https://test.com/api", null, HttpMethod.Get, - new KeyValuePair[0], + new HttpRequestMessage().Headers, ResultDataSource.Server, new TestObjectResult(), null); @@ -146,7 +143,7 @@ namespace CryptoExchange.Net.UnitTests var result = new WebCallResult( System.Net.HttpStatusCode.OK, HttpVersion.Version11, - new KeyValuePair[0], + new HttpResponseMessage().Headers, TimeSpan.FromSeconds(1), null, "{}", @@ -154,7 +151,7 @@ namespace CryptoExchange.Net.UnitTests "https://test.com/api", null, HttpMethod.Get, - new KeyValuePair[0], + new HttpRequestMessage().Headers, ResultDataSource.Server, new TestObjectResult(), null); diff --git a/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj b/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj index f463766..35dbd07 100644 --- a/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj +++ b/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj @@ -1,15 +1,15 @@  - net9.0 + net10.0 false - + - + diff --git a/CryptoExchange.Net.UnitTests/ExchangeHelpersTests.cs b/CryptoExchange.Net.UnitTests/ExchangeHelpersTests.cs index 2fcdae0..a9808fb 100644 --- a/CryptoExchange.Net.UnitTests/ExchangeHelpersTests.cs +++ b/CryptoExchange.Net.UnitTests/ExchangeHelpersTests.cs @@ -1,7 +1,5 @@ using CryptoExchange.Net.Objects; using NUnit.Framework; -using NUnit.Framework.Legacy; -using System.Diagnostics; using System.Globalization; namespace CryptoExchange.Net.UnitTests diff --git a/CryptoExchange.Net.UnitTests/OptionsTests.cs b/CryptoExchange.Net.UnitTests/OptionsTests.cs index 7af2ac2..3f4340d 100644 --- a/CryptoExchange.Net.UnitTests/OptionsTests.cs +++ b/CryptoExchange.Net.UnitTests/OptionsTests.cs @@ -1,15 +1,8 @@ using CryptoExchange.Net.Authentication; -using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.UnitTests.TestImplementations; -using Microsoft.Extensions.Logging; using NUnit.Framework; -using NUnit.Framework.Legacy; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace CryptoExchange.Net.UnitTests { diff --git a/CryptoExchange.Net.UnitTests/RestClientTests.cs b/CryptoExchange.Net.UnitTests/RestClientTests.cs index 57f1f08..e7c790b 100644 --- a/CryptoExchange.Net.UnitTests/RestClientTests.cs +++ b/CryptoExchange.Net.UnitTests/RestClientTests.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using System.Threading; using NUnit.Framework.Legacy; using CryptoExchange.Net.RateLimiting; -using System.Net; using CryptoExchange.Net.RateLimiting.Guards; using CryptoExchange.Net.RateLimiting.Filters; using CryptoExchange.Net.RateLimiting.Interfaces; diff --git a/CryptoExchange.Net.UnitTests/SocketClientTests.cs b/CryptoExchange.Net.UnitTests/SocketClientTests.cs index 1273221..97ce6a6 100644 --- a/CryptoExchange.Net.UnitTests/SocketClientTests.cs +++ b/CryptoExchange.Net.UnitTests/SocketClientTests.cs @@ -1,231 +1,234 @@ -using System; -using System.Collections.Generic; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using CryptoExchange.Net.Objects; -using CryptoExchange.Net.Objects.Sockets; -using CryptoExchange.Net.Sockets; -using CryptoExchange.Net.UnitTests.TestImplementations; -using CryptoExchange.Net.UnitTests.TestImplementations.Sockets; -using Microsoft.Extensions.Logging; -using Moq; -using NUnit.Framework; -using NUnit.Framework.Legacy; +//using CryptoExchange.Net.Objects; +//using CryptoExchange.Net.Objects.Sockets; +//using CryptoExchange.Net.Sockets; +//using CryptoExchange.Net.Testing.Implementations; +//using CryptoExchange.Net.UnitTests.TestImplementations; +//using CryptoExchange.Net.UnitTests.TestImplementations.Sockets; +//using Microsoft.Extensions.Logging; +//using Moq; +//using NUnit.Framework; +//using NUnit.Framework.Legacy; +//using System; +//using System.Collections.Generic; +//using System.Net.Sockets; +//using System.Text.Json; +//using System.Threading; +//using System.Threading.Tasks; -namespace CryptoExchange.Net.UnitTests -{ - [TestFixture] - public class SocketClientTests - { - [TestCase] - public void SettingOptions_Should_ResultInOptionsSet() - { - //arrange - //act - var client = new TestSocketClient(options => - { - options.SubOptions.ApiCredentials = new Authentication.ApiCredentials("1", "2"); - options.SubOptions.MaxSocketConnections = 1; - }); +//namespace CryptoExchange.Net.UnitTests +//{ +// [TestFixture] +// public class SocketClientTests +// { +// [TestCase] +// public void SettingOptions_Should_ResultInOptionsSet() +// { +// //arrange +// //act +// var client = new TestSocketClient(options => +// { +// options.SubOptions.ApiCredentials = new Authentication.ApiCredentials("1", "2"); +// options.SubOptions.MaxSocketConnections = 1; +// }); - //assert - ClassicAssert.NotNull(client.SubClient.ApiOptions.ApiCredentials); - Assert.That(1 == client.SubClient.ApiOptions.MaxSocketConnections); - } +// //assert +// ClassicAssert.NotNull(client.SubClient.ApiOptions.ApiCredentials); +// Assert.That(1 == client.SubClient.ApiOptions.MaxSocketConnections); +// } - [TestCase(true)] - [TestCase(false)] - public void ConnectSocket_Should_ReturnConnectionResult(bool canConnect) - { - //arrange - var client = new TestSocketClient(); - var socket = client.CreateSocket(); - socket.CanConnect = canConnect; +// [TestCase(true)] +// [TestCase(false)] +// public void ConnectSocket_Should_ReturnConnectionResult(bool canConnect) +// { +// //arrange +// var client = new TestSocketClient(); +// var socket = client.CreateSocket(); +// socket.CanConnect = canConnect; - //act - var connectResult = client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, null)); +// //act +// var connectResult = client.SubClient.ConnectSocketSub( +// new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "")); - //assert - Assert.That(connectResult.Success == canConnect); - } +// //assert +// Assert.That(connectResult.Success == canConnect); +// } - [TestCase] - public void SocketMessages_Should_BeProcessedInDataHandlers() - { - // arrange - var client = new TestSocketClient(options => { - options.ReconnectInterval = TimeSpan.Zero; - }); - var socket = client.CreateSocket(); - socket.CanConnect = true; - var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null); - var rstEvent = new ManualResetEvent(false); - Dictionary result = null; +// [TestCase] +// public void SocketMessages_Should_BeProcessedInDataHandlers() +// { +// // arrange +// var client = new TestSocketClient(options => { +// options.ReconnectInterval = TimeSpan.Zero; +// }); +// var socket = client.CreateSocket(); +// socket.CanConnect = true; +// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""); +// var rstEvent = new ManualResetEvent(false); +// Dictionary result = null; - client.SubClient.ConnectSocketSub(sub); +// client.SubClient.ConnectSocketSub(sub); - var subObj = new TestSubscription>(Mock.Of(), (messageEvent) => - { - result = messageEvent.Data; - rstEvent.Set(); - }); - sub.AddSubscription(subObj); +// var subObj = new TestSubscription>(Mock.Of(), (messageEvent) => +// { +// result = messageEvent.Data; +// rstEvent.Set(); +// }); +// sub.AddSubscription(subObj); - // act - socket.InvokeMessage("{\"property\": \"123\", \"action\": \"update\", \"topic\": \"topic\"}"); - rstEvent.WaitOne(1000); +// // act +// socket.InvokeMessage("{\"property\": \"123\", \"action\": \"update\", \"topic\": \"topic\"}"); +// rstEvent.WaitOne(1000); - // assert - Assert.That(result["property"] == "123"); - } +// // assert +// Assert.That(result["property"] == "123"); +// } - [TestCase(false)] - [TestCase(true)] - public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled) - { - // arrange - var client = new TestSocketClient(options => - { - options.ReconnectInterval = TimeSpan.Zero; - options.SubOptions.OutputOriginalData = enabled; - }); - var socket = client.CreateSocket(); - socket.CanConnect = true; - var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null); - var rstEvent = new ManualResetEvent(false); - string original = null; +// [TestCase(false)] +// [TestCase(true)] +// public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled) +// { +// // arrange +// var client = new TestSocketClient(options => +// { +// options.ReconnectInterval = TimeSpan.Zero; +// options.SubOptions.OutputOriginalData = enabled; +// }); +// var socket = client.CreateSocket(); +// socket.CanConnect = true; +// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""); +// var rstEvent = new ManualResetEvent(false); +// string original = null; - client.SubClient.ConnectSocketSub(sub); - var subObj = new TestSubscription>(Mock.Of(), (messageEvent) => - { - original = messageEvent.OriginalData; - rstEvent.Set(); - }); - sub.AddSubscription(subObj); - var msgToSend = JsonSerializer.Serialize(new { topic = "topic", action = "update", property = "123" }); +// client.SubClient.ConnectSocketSub(sub); +// var subObj = new TestSubscription>(Mock.Of(), (messageEvent) => +// { +// original = messageEvent.OriginalData; +// rstEvent.Set(); +// }); +// sub.AddSubscription(subObj); +// var msgToSend = JsonSerializer.Serialize(new { topic = "topic", action = "update", property = "123" }); - // act - socket.InvokeMessage(msgToSend); - rstEvent.WaitOne(1000); +// // act +// socket.InvokeMessage(msgToSend); +// rstEvent.WaitOne(1000); - // assert - Assert.That(original == (enabled ? msgToSend : null)); - } +// // assert +// Assert.That(original == (enabled ? msgToSend : null)); +// } - [TestCase()] - public void UnsubscribingStream_Should_CloseTheSocket() - { - // arrange - var client = new TestSocketClient(options => - { - options.ReconnectInterval = TimeSpan.Zero; - }); - var socket = client.CreateSocket(); - socket.CanConnect = true; - var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null); - client.SubClient.ConnectSocketSub(sub); +// [TestCase()] +// public void UnsubscribingStream_Should_CloseTheSocket() +// { +// // arrange +// var client = new TestSocketClient(options => +// { +// options.ReconnectInterval = TimeSpan.Zero; +// }); +// var socket = client.CreateSocket(); +// socket.CanConnect = true; +// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""); +// client.SubClient.ConnectSocketSub(sub); - var subscription = new TestSubscription>(Mock.Of(), (messageEvent) => { }); - var ups = new UpdateSubscription(sub, subscription); - sub.AddSubscription(subscription); +// var subscription = new TestSubscription>(Mock.Of(), (messageEvent) => { }); +// var ups = new UpdateSubscription(sub, subscription); +// sub.AddSubscription(subscription); - // act - client.UnsubscribeAsync(ups).Wait(); +// // act +// client.UnsubscribeAsync(ups).Wait(); - // assert - Assert.That(socket.Connected == false); - } +// // assert +// Assert.That(socket.Connected == false); +// } - [TestCase()] - public void UnsubscribingAll_Should_CloseAllSockets() - { - // arrange - var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; }); - var socket1 = client.CreateSocket(); - var socket2 = client.CreateSocket(); - socket1.CanConnect = true; - socket2.CanConnect = true; - var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket1, null); - var sub2 = new SocketConnection(new TraceLogger(), client.SubClient, socket2, null); - client.SubClient.ConnectSocketSub(sub1); - client.SubClient.ConnectSocketSub(sub2); - var subscription1 = new TestSubscription>(Mock.Of(), (messageEvent) => { }); - var subscription2 = new TestSubscription>(Mock.Of(), (messageEvent) => { }); +// [TestCase()] +// public void UnsubscribingAll_Should_CloseAllSockets() +// { +// // arrange +// var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; }); +// var socket1 = client.CreateSocket(); +// var socket2 = client.CreateSocket(); +// socket1.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 sub2 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket2), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""); +// client.SubClient.ConnectSocketSub(sub1); +// client.SubClient.ConnectSocketSub(sub2); +// var subscription1 = new TestSubscription>(Mock.Of(), (messageEvent) => { }); +// var subscription2 = new TestSubscription>(Mock.Of(), (messageEvent) => { }); - sub1.AddSubscription(subscription1); - sub2.AddSubscription(subscription2); - var ups1 = new UpdateSubscription(sub1, subscription1); - var ups2 = new UpdateSubscription(sub2, subscription2); +// sub1.AddSubscription(subscription1); +// sub2.AddSubscription(subscription2); +// var ups1 = new UpdateSubscription(sub1, subscription1); +// var ups2 = new UpdateSubscription(sub2, subscription2); - // act - client.UnsubscribeAllAsync().Wait(); +// // act +// client.UnsubscribeAllAsync().Wait(); - // assert - Assert.That(socket1.Connected == false); - Assert.That(socket2.Connected == false); - } +// // assert +// Assert.That(socket1.Connected == false); +// Assert.That(socket2.Connected == false); +// } - [TestCase()] - public void FailingToConnectSocket_Should_ReturnError() - { - // arrange - var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; }); - var socket = client.CreateSocket(); - socket.CanConnect = false; - var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket, null); +// [TestCase()] +// public void FailingToConnectSocket_Should_ReturnError() +// { +// // arrange +// var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; }); +// var socket = client.CreateSocket(); +// socket.CanConnect = false; +// var sub1 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""); - // act - var connectResult = client.SubClient.ConnectSocketSub(sub1); +// // act +// var connectResult = client.SubClient.ConnectSocketSub(sub1); - // assert - ClassicAssert.IsFalse(connectResult.Success); - } +// // assert +// ClassicAssert.IsFalse(connectResult.Success); +// } - [TestCase()] - public async Task ErrorResponse_ShouldNot_ConfirmSubscription() - { - // arrange - var channel = "trade_btcusd"; - var client = new TestSocketClient(opt => - { - opt.OutputOriginalData = true; - opt.SocketSubscriptionsCombineTarget = 1; - }); - var socket = client.CreateSocket(); - socket.CanConnect = true; - client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, "https://test.test")); +// [TestCase()] +// public async Task ErrorResponse_ShouldNot_ConfirmSubscription() +// { +// // arrange +// var channel = "trade_btcusd"; +// var client = new TestSocketClient(opt => +// { +// opt.OutputOriginalData = true; +// opt.SocketSubscriptionsCombineTarget = 1; +// }); +// var socket = client.CreateSocket(); +// socket.CanConnect = true; +// client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "")); - // act - var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default); - socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "error" })); - await sub; +// // act +// var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default); +// socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "error" })); +// await sub; - // assert - ClassicAssert.IsTrue(client.SubClient.TestSubscription.Status != SubscriptionStatus.Subscribed); - } +// // assert +// ClassicAssert.IsTrue(client.SubClient.TestSubscription.Status != SubscriptionStatus.Subscribed); +// } - [TestCase()] - public async Task SuccessResponse_Should_ConfirmSubscription() - { - // arrange - var channel = "trade_btcusd"; - var client = new TestSocketClient(opt => - { - opt.OutputOriginalData = true; - opt.SocketSubscriptionsCombineTarget = 1; - }); - var socket = client.CreateSocket(); - socket.CanConnect = true; - client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, "https://test.test")); +// [TestCase()] +// public async Task SuccessResponse_Should_ConfirmSubscription() +// { +// // arrange +// var channel = "trade_btcusd"; +// var client = new TestSocketClient(opt => +// { +// opt.OutputOriginalData = true; +// opt.SocketSubscriptionsCombineTarget = 1; +// }); +// var socket = client.CreateSocket(); +// socket.CanConnect = true; +// client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "")); - // act - var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default); - socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "confirmed" })); - await sub; +// // act +// var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default); +// socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "confirmed" })); +// await sub; - // assert - Assert.That(client.SubClient.TestSubscription.Status == SubscriptionStatus.Subscribed); - } - } -} +// // assert +// Assert.That(client.SubClient.TestSubscription.Status == SubscriptionStatus.Subscribed); +// } +// } +//} diff --git a/CryptoExchange.Net.UnitTests/SystemTextJsonConverterTests.cs b/CryptoExchange.Net.UnitTests/SystemTextJsonConverterTests.cs index 3270ffc..755f79f 100644 --- a/CryptoExchange.Net.UnitTests/SystemTextJsonConverterTests.cs +++ b/CryptoExchange.Net.UnitTests/SystemTextJsonConverterTests.cs @@ -4,9 +4,7 @@ using System.Text.Json; using NUnit.Framework; using System; using System.Text.Json.Serialization; -using NUnit.Framework.Legacy; using CryptoExchange.Net.Converters; -using CryptoExchange.Net.Testing.Comparers; using CryptoExchange.Net.SharedApis; namespace CryptoExchange.Net.UnitTests diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestChannelQuery.cs b/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestChannelQuery.cs deleted file mode 100644 index 431dd7a..0000000 --- a/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestChannelQuery.cs +++ /dev/null @@ -1,50 +0,0 @@ -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 - { - public TestChannelQuery(string channel, string request, bool authenticated, int weight = 1) : base(request, authenticated, weight) - { - MessageMatcher = MessageMatcher.Create(request + "-" + channel, HandleMessage); - } - - public CallResult HandleMessage(SocketConnection connection, DataEvent message) - { - if (!message.Data.Status.Equals("confirmed", StringComparison.OrdinalIgnoreCase)) - { - return new CallResult(new ServerError(ErrorInfo.Unknown with { Message = message.Data.Status })); - } - - return message.ToCallResult(); - } - } -} diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestQuery.cs b/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestQuery.cs deleted file mode 100644 index f2302b6..0000000 --- a/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestQuery.cs +++ /dev/null @@ -1,17 +0,0 @@ -using CryptoExchange.Net.Sockets; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets -{ - internal class TestQuery : Query - { - public TestQuery(string identifier, object request, bool authenticated, int weight = 1) : base(request, authenticated, weight) - { - MessageMatcher = MessageMatcher.Create(identifier); - } - } -} diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestSubscription.cs b/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestSubscription.cs deleted file mode 100644 index f9cb121..0000000 --- a/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestSubscription.cs +++ /dev/null @@ -1,34 +0,0 @@ -using CryptoExchange.Net.Interfaces; -using CryptoExchange.Net.Objects; -using CryptoExchange.Net.Objects.Sockets; -using CryptoExchange.Net.Sockets; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets -{ - internal class TestSubscription : Subscription - { - private readonly Action> _handler; - - public TestSubscription(ILogger logger, Action> handler) : base(logger, false) - { - _handler = handler; - - MessageMatcher = MessageMatcher.Create("update-topic", DoHandleMessage); - } - - public CallResult DoHandleMessage(SocketConnection connection, DataEvent 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); - } -} diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestSubscriptionWithResponseCheck.cs b/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestSubscriptionWithResponseCheck.cs deleted file mode 100644 index 6c1e616..0000000 --- a/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestSubscriptionWithResponseCheck.cs +++ /dev/null @@ -1,34 +0,0 @@ -using CryptoExchange.Net.Interfaces; -using CryptoExchange.Net.Objects; -using CryptoExchange.Net.Objects.Sockets; -using CryptoExchange.Net.Sockets; -using Microsoft.Extensions.Logging; -using Moq; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets -{ - internal class TestSubscriptionWithResponseCheck : Subscription - { - private readonly Action> _handler; - private readonly string _channel; - - public TestSubscriptionWithResponseCheck(string channel, Action> handler) : base(Mock.Of(), false) - { - MessageMatcher = MessageMatcher.Create(channel, DoHandleMessage); - _handler = handler; - _channel = channel; - } - - public CallResult DoHandleMessage(SocketConnection connection, DataEvent 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); - } -} diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs index 176cff4..2d08c2b 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs @@ -1,19 +1,16 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Net.Http; using System.Text; -using System.Text.Json.Serialization; using System.Threading.Tasks; using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Clients; +using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters; using CryptoExchange.Net.Converters.SystemTextJson; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Errors; using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.SharedApis; -using CryptoExchange.Net.UnitTests.TestImplementations; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -46,6 +43,8 @@ namespace CryptoExchange.Net.UnitTests public class TestSubClient : RestApiClient { + protected override IRestMessageHandler MessageHandler => throw new NotImplementedException(); + public TestSubClient(RestExchangeOptions options, RestApiOptions apiOptions) : base(new TraceLogger(), null, "https://localhost:123", options, apiOptions) { } @@ -74,6 +73,8 @@ namespace CryptoExchange.Net.UnitTests public class TestAuthProvider : AuthenticationProvider { + public override ApiCredentialsType[] SupportedCredentialTypes => [ApiCredentialsType.Hmac]; + public TestAuthProvider(ApiCredentials credentials) : base(credentials) { } @@ -85,4 +86,14 @@ namespace CryptoExchange.Net.UnitTests public string GetKey() => _credentials.Key; public string GetSecret() => _credentials.Secret; } + + public class TestEnvironment : TradeEnvironment + { + public string TestAddress { get; } + + public TestEnvironment(string name, string url) : base(name) + { + TestAddress = url; + } + } } diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs index f81ce43..2bb242a 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs @@ -13,12 +13,13 @@ using CryptoExchange.Net.Authentication; using System.Collections.Generic; using Microsoft.Extensions.Logging; using CryptoExchange.Net.Clients; -using CryptoExchange.Net.SharedApis; using Microsoft.Extensions.Options; using System.Linq; using CryptoExchange.Net.Converters.SystemTextJson; using System.Text.Json.Serialization; -using CryptoExchange.Net.Objects.Errors; +using System.Net.Http.Headers; +using CryptoExchange.Net.SharedApis; +using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters; namespace CryptoExchange.Net.UnitTests.TestImplementations { @@ -51,13 +52,13 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations response.Setup(c => c.IsSuccessStatusCode).Returns(true); response.Setup(c => c.GetResponseStreamAsync(It.IsAny())).Returns(Task.FromResult((Stream)responseStream)); - var headers = new Dictionary(); + var headers = new HttpRequestMessage().Headers; var request = new Mock(); request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com")); request.Setup(c => c.GetResponseAsync(It.IsAny())).Returns(Task.FromResult(response.Object)); request.Setup(c => c.SetContent(It.IsAny(), It.IsAny())).Callback(new Action((content, type) => { request.Setup(r => r.Content).Returns(content); })); request.Setup(c => c.AddHeader(It.IsAny(), It.IsAny())).Callback((key, val) => headers.Add(key, new string[] { val })); - request.Setup(c => c.GetHeaders()).Returns(() => headers.ToArray()); + request.Setup(c => c.GetHeaders()).Returns(() => headers); var factory = Mock.Get(Api1.RequestFactory); factory.Setup(c => c.Create(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) @@ -86,7 +87,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations var request = new Mock(); request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com")); - request.Setup(c => c.GetHeaders()).Returns(new KeyValuePair[0]); + request.Setup(c => c.GetHeaders()).Returns(new HttpRequestMessage().Headers); request.Setup(c => c.GetResponseAsync(It.IsAny())).Throws(we); var factory = Mock.Get(Api1.RequestFactory); @@ -115,7 +116,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com")); request.Setup(c => c.GetResponseAsync(It.IsAny())).Returns(Task.FromResult(response.Object)); request.Setup(c => c.AddHeader(It.IsAny(), It.IsAny())).Callback((key, val) => headers.Add(new KeyValuePair(key, new string[] { val }))); - request.Setup(c => c.GetHeaders()).Returns(headers.ToArray()); + request.Setup(c => c.GetHeaders()).Returns(new HttpRequestMessage().Headers); var factory = Mock.Get(Api1.RequestFactory); factory.Setup(c => c.Create(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) @@ -131,6 +132,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations 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) { RequestFactory = new Mock().Object; @@ -178,6 +181,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations 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) { RequestFactory = new Mock().Object; @@ -194,13 +199,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations return await SendAsync("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct); } - protected override Error ParseErrorResponse(int httpStatusCode, KeyValuePair[] responseHeaders, IMessageAccessor accessor, Exception exception) - { - var errorData = accessor.Deserialize(); - - return new ServerError(errorData.Data.ErrorCode, GetErrorInfo(errorData.Data.ErrorCode, errorData.Data.ErrorMessage)); - } - public override TimeSpan? GetTimeOffset() { throw new NotImplementedException(); diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestMessageHandler.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestMessageHandler.cs new file mode 100644 index 0000000..d2ea822 --- /dev/null +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestMessageHandler.cs @@ -0,0 +1,29 @@ +using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters; +using CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Errors; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace CryptoExchange.Net.UnitTests.TestImplementations +{ + internal class TestRestMessageHandler : JsonRestMessageHandler + { + private ErrorMapping _errorMapping = new ErrorMapping([]); + public override JsonSerializerOptions Options => new JsonSerializerOptions(); + + public override ValueTask ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream) + { + var errorData = JsonSerializer.Deserialize(responseStream); + + return new ValueTask(new ServerError(errorData.ErrorCode, _errorMapping.GetErrorInfo(errorData.ErrorCode.ToString(), errorData.ErrorMessage))); + } + } +} diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocket.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocket.cs deleted file mode 100644 index c3408de..0000000 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocket.cs +++ /dev/null @@ -1,132 +0,0 @@ -//using System; -//using System.IO; -//using System.Net.WebSockets; -//using System.Security.Authentication; -//using System.Text; -//using System.Threading.Tasks; -//using CryptoExchange.Net.Interfaces; -//using CryptoExchange.Net.Objects; - -//namespace CryptoExchange.Net.UnitTests.TestImplementations -//{ -// public class TestSocket: IWebsocket -// { -// public bool CanConnect { get; set; } -// public bool Connected { get; set; } - -// public event Func OnClose; -//#pragma warning disable 0067 -// public event Func OnReconnected; -// public event Func OnReconnecting; -// public event Func OnRequestRateLimited; -//#pragma warning restore 0067 -// public event Func OnRequestSent; -// public event Func, Task> OnStreamMessage; -// public event Func OnError; -// public event Func OnOpen; -// public Func> GetReconnectionUrl { get; set; } - -// public int Id { get; } -// public bool ShouldReconnect { get; set; } -// public TimeSpan Timeout { get; set; } -// public Func DataInterpreterString { get; set; } -// public Func DataInterpreterBytes { get; set; } -// public DateTime? DisconnectTime { get; set; } -// public string Url { get; } -// public bool IsClosed => !Connected; -// public bool IsOpen => Connected; -// public bool PingConnection { get; set; } -// public TimeSpan PingInterval { get; set; } -// public SslProtocols SSLProtocols { get; set; } -// public Encoding Encoding { get; set; } - -// public int ConnectCalls { get; private set; } -// public bool Reconnecting { get; set; } -// public string Origin { get; set; } -// public int? RatelimitPerSecond { get; set; } - -// public double IncomingKbps => throw new NotImplementedException(); - -// public Uri Uri => new Uri(""); - -// public TimeSpan KeepAliveInterval { get; set; } - -// public static int lastId = 0; -// public static object lastIdLock = new object(); - -// public TestSocket() -// { -// lock (lastIdLock) -// { -// Id = lastId + 1; -// lastId++; -// } -// } - -// public Task ConnectAsync() -// { -// Connected = CanConnect; -// ConnectCalls++; -// if (CanConnect) -// InvokeOpen(); -// return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError())); -// } - -// public bool Send(int requestId, string data, int weight) -// { -// if(!Connected) -// throw new Exception("Socket not connected"); -// OnRequestSent?.Invoke(requestId); -// return true; -// } - -// public void Reset() -// { -// } - -// public Task CloseAsync() -// { -// Connected = false; -// DisconnectTime = DateTime.UtcNow; -// OnClose?.Invoke(); -// return Task.FromResult(0); -// } - -// public void SetProxy(string host, int port) -// { -// throw new NotImplementedException(); -// } -// public void Dispose() -// { -// } - -// public void InvokeClose() -// { -// Connected = false; -// DisconnectTime = DateTime.UtcNow; -// Reconnecting = true; -// OnClose?.Invoke(); -// } - -// public void InvokeOpen() -// { -// OnOpen?.Invoke(); -// } - -// public void InvokeMessage(string data) -// { -// OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory(Encoding.UTF8.GetBytes(data))).Wait(); -// } - -// public void SetProxy(ApiProxy proxy) -// { -// throw new NotImplementedException(); -// } - -// public void InvokeError(Exception error) -// { -// OnError?.Invoke(error); -// } -// public Task ReconnectAsync() => Task.CompletedTask; -// } -//} diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs deleted file mode 100644 index 9df7288..0000000 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using CryptoExchange.Net.Authentication; -using CryptoExchange.Net.Clients; -using CryptoExchange.Net.Converters.MessageParsing; -using CryptoExchange.Net.Interfaces; -using CryptoExchange.Net.Objects; -using CryptoExchange.Net.Objects.Options; -using CryptoExchange.Net.Objects.Sockets; -using CryptoExchange.Net.Sockets; -using CryptoExchange.Net.UnitTests.TestImplementations.Sockets; -using Microsoft.Extensions.Logging; -using Moq; -using CryptoExchange.Net.Testing.Implementations; -using CryptoExchange.Net.SharedApis; -using Microsoft.Extensions.Options; -using CryptoExchange.Net.Converters.SystemTextJson; -using System.Net.WebSockets; - -namespace CryptoExchange.Net.UnitTests.TestImplementations -{ - internal class TestSocketClient: BaseSocketClient - { - public TestSubSocketClient SubClient { get; } - - /// - /// Create a new instance of KucoinSocketClient - /// - /// Configure the options to use for this client - public TestSocketClient(Action optionsDelegate = null) - : this(Options.Create(ApplyOptionsDelegate(optionsDelegate)), null) - { - } - - public TestSocketClient(IOptions options, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test") - { - Initialize(options.Value); - - SubClient = AddApiClient(new TestSubSocketClient(options.Value, options.Value.SubOptions)); - SubClient.SocketFactory = new Mock().Object; - Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny(), It.IsAny())).Returns(new TestSocket("https://test.com")); - } - - public TestSocket CreateSocket() - { - Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny(), It.IsAny())).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 - { - public static TestSocketOptions Default = new TestSocketOptions - { - Environment = new TestEnvironment("Live", "https://test.test") - }; - - /// - /// ctor - /// - public TestSocketOptions() - { - Default?.Set(this); - } - - public SocketApiOptions SubOptions { get; set; } = new SocketApiOptions(); - - internal TestSocketOptions Set(TestSocketOptions targetOptions) - { - targetOptions = base.Set(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()); - - /// - 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(_channelPath); - id ??= message.GetValue(_topicPath); - - return message.GetValue(_actionPath) + "-" + id; - } - - public Task> SubscribeToSomethingAsync(string channel, Action> onUpdate, CancellationToken ct) - { - TestSubscription = new TestSubscriptionWithResponseCheck(channel, onUpdate); - return SubscribeAsync(TestSubscription, ct); - } - } -} diff --git a/CryptoExchange.Net.UnitTests/TestSerializerContext.cs b/CryptoExchange.Net.UnitTests/TestSerializerContext.cs index e3dfe5b..ce23994 100644 --- a/CryptoExchange.Net.UnitTests/TestSerializerContext.cs +++ b/CryptoExchange.Net.UnitTests/TestSerializerContext.cs @@ -1,10 +1,6 @@ using CryptoExchange.Net.UnitTests.TestImplementations; -using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Text.Json.Serialization; -using System.Threading.Tasks; namespace CryptoExchange.Net.UnitTests { diff --git a/CryptoExchange.Net/Authentication/ApiCredentials.cs b/CryptoExchange.Net/Authentication/ApiCredentials.cs index 3542b3b..ac3f332 100644 --- a/CryptoExchange.Net/Authentication/ApiCredentials.cs +++ b/CryptoExchange.Net/Authentication/ApiCredentials.cs @@ -1,7 +1,6 @@ using System; using System.IO; -using CryptoExchange.Net.Converters.SystemTextJson; -using CryptoExchange.Net.Converters.MessageParsing; +using System.Threading.Tasks; namespace CryptoExchange.Net.Authentication { @@ -48,6 +47,48 @@ namespace CryptoExchange.Net.Authentication Pass = pass; } + /// + /// Create API credentials using an API key and secret generated by the server + /// + public static ApiCredentials HmacCredentials(string apiKey, string apiSecret, string? pass) + { + return new ApiCredentials(apiKey, apiSecret, pass, ApiCredentialsType.Hmac); + } + + /// + /// Create API credentials using an API key and an RSA private key in PEM format + /// + public static ApiCredentials RsaPemCredentials(string apiKey, string privateKey) + { + return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.RsaPem); + } + + /// + /// Create API credentials using an API key and an RSA private key in XML format + /// + public static ApiCredentials RsaXmlCredentials(string apiKey, string privateKey) + { + return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.RsaXml); + } + + /// + /// Create API credentials using an API key and an Ed25519 private key + /// + public static ApiCredentials Ed25519Credentials(string apiKey, string privateKey) + { + return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.Ed25519); + } + + /// + /// Load a key from a file + /// + public static string ReadFromFile(string path) + { + using var fileStream = File.OpenRead(path); + using var streamReader = new StreamReader(fileStream); + return streamReader.ReadToEnd(); + } + /// /// Copy the credentials /// diff --git a/CryptoExchange.Net/Authentication/ApiCredentialsType.cs b/CryptoExchange.Net/Authentication/ApiCredentialsType.cs index 2da474f..63b0281 100644 --- a/CryptoExchange.Net/Authentication/ApiCredentialsType.cs +++ b/CryptoExchange.Net/Authentication/ApiCredentialsType.cs @@ -16,6 +16,10 @@ /// /// Rsa keys credentials in pem/base64 format. Only available for .NetStandard 2.1 and up, use xml format for lower. /// - RsaPem + RsaPem, + /// + /// Ed25519 keys credentials + /// + Ed25519 } } diff --git a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs index 1c49f1d..7a4112f 100644 --- a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs +++ b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs @@ -2,10 +2,13 @@ using CryptoExchange.Net.Converters.SystemTextJson; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; +#if NET8_0_OR_GREATER +using NSec.Cryptography; +#endif using System; using System.Collections.Generic; +using System.Linq; using System.Globalization; -using System.Net.Http; using System.Security.Cryptography; using System.Text; @@ -18,6 +21,11 @@ namespace CryptoExchange.Net.Authentication { internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider(); + /// + /// The supported credential types + /// + public abstract ApiCredentialsType[] SupportedCredentialTypes { get; } + /// /// Provided credentials /// @@ -28,6 +36,13 @@ namespace CryptoExchange.Net.Authentication /// protected byte[] _sBytes; +#if NET8_0_OR_GREATER + /// + /// The Ed25519 private key + /// + protected Key? Ed25519Key; +#endif + /// /// Get the API key of the current credentials /// @@ -46,6 +61,16 @@ namespace CryptoExchange.Net.Authentication if (credentials.Key == null || credentials.Secret == null) throw new ArgumentException("ApiKey/Secret needed"); + if (!SupportedCredentialTypes.Any(x => x == credentials.CredentialType)) + throw new ArgumentException($"Credential type {credentials.CredentialType} not supported"); + + if (credentials.CredentialType == ApiCredentialsType.Ed25519) + { +#if !NET8_0_OR_GREATER + throw new ArgumentException($"Credential type Ed25519 only supported on Net8.0 or newer"); +#endif + } + _credentials = credentials; _sBytes = Encoding.UTF8.GetBytes(credentials.Secret); } @@ -349,6 +374,36 @@ namespace CryptoExchange.Net.Authentication return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); } + /// + /// Ed25519 sign the data + /// + public string SignEd25519(string data, SignOutputType? outputType = null) + => SignEd25519(Encoding.ASCII.GetBytes(data), outputType); + + /// + /// Ed25519 sign the data + /// + public string SignEd25519(byte[] data, SignOutputType? outputType = null) + { +#if NET8_0_OR_GREATER + if (Ed25519Key == null) + { + var key = _credentials.Secret! + .Replace("\n", "") + .Replace("-----BEGIN PRIVATE KEY-----", "") + .Replace("-----END PRIVATE KEY-----", "") + .Trim(); + var keyBytes = Convert.FromBase64String(key); + Ed25519Key = Key.Import(SignatureAlgorithm.Ed25519, keyBytes, KeyBlobFormat.PkixPrivateKey); + } + + var resultBytes = SignatureAlgorithm.Ed25519.Sign(Ed25519Key, data); + return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); +#else + throw new InvalidOperationException(); +#endif + } + private RSA CreateRSA() { var rsa = RSA.Create(); @@ -449,7 +504,7 @@ namespace CryptoExchange.Net.Authentication if (serializer is not IStringMessageSerializer stringSerializer) throw new InvalidOperationException("Non-string message serializer can't get serialized request body"); - if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value)) + if (parameters?.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value)) return stringSerializer.Serialize(value); else return stringSerializer.Serialize(parameters); diff --git a/CryptoExchange.Net/Caching/MemoryCache.cs b/CryptoExchange.Net/Caching/MemoryCache.cs index ca2c3c4..4507ac6 100644 --- a/CryptoExchange.Net/Caching/MemoryCache.cs +++ b/CryptoExchange.Net/Caching/MemoryCache.cs @@ -1,13 +1,18 @@ using System; using System.Collections.Concurrent; using System.Linq; +using System.Threading; namespace CryptoExchange.Net.Caching { internal class MemoryCache { private readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); +#if NET9_0_OR_GREATER + private readonly Lock _lock = new Lock(); +#else private readonly object _lock = new object(); +#endif /// /// Add a new cache entry. Will override an existing entry if it already exists diff --git a/CryptoExchange.Net/Clients/BaseApiClient.cs b/CryptoExchange.Net/Clients/BaseApiClient.cs index 7806e1f..c2eab52 100644 --- a/CryptoExchange.Net/Clients/BaseApiClient.cs +++ b/CryptoExchange.Net/Clients/BaseApiClient.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; using CryptoExchange.Net.Authentication; -using CryptoExchange.Net.Interfaces; -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Interfaces.Clients; using CryptoExchange.Net.Objects.Errors; using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.SharedApis; diff --git a/CryptoExchange.Net/Clients/BaseClient.cs b/CryptoExchange.Net/Clients/BaseClient.cs index 5d3fc5f..919346e 100644 --- a/CryptoExchange.Net/Clients/BaseClient.cs +++ b/CryptoExchange.Net/Clients/BaseClient.cs @@ -1,9 +1,9 @@ using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Objects.Options; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using System; using System.Collections.Generic; +using System.Threading; namespace CryptoExchange.Net.Clients { @@ -49,7 +49,11 @@ namespace CryptoExchange.Net.Clients /// protected internal ILogger _logger; +#if NET9_0_OR_GREATER + private readonly Lock _versionLock = new Lock(); +#else private readonly object _versionLock = new object(); +#endif private Version _exchangeVersion; /// diff --git a/CryptoExchange.Net/Clients/BaseRestClient.cs b/CryptoExchange.Net/Clients/BaseRestClient.cs index 797dace..057127d 100644 --- a/CryptoExchange.Net/Clients/BaseRestClient.cs +++ b/CryptoExchange.Net/Clients/BaseRestClient.cs @@ -1,5 +1,5 @@ using System.Linq; -using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Interfaces.Clients; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; diff --git a/CryptoExchange.Net/Clients/BaseSocketClient.cs b/CryptoExchange.Net/Clients/BaseSocketClient.cs index 3f36513..6296998 100644 --- a/CryptoExchange.Net/Clients/BaseSocketClient.cs +++ b/CryptoExchange.Net/Clients/BaseSocketClient.cs @@ -1,14 +1,14 @@ -using System; +using CryptoExchange.Net.Interfaces.Clients; +using CryptoExchange.Net.Logging.Extensions; +using CryptoExchange.Net.Objects.Options; +using CryptoExchange.Net.Objects.Sockets; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -using System.Xml.Linq; -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 { @@ -30,6 +30,9 @@ namespace CryptoExchange.Net.Clients public int CurrentSubscriptions => ApiClients.OfType().Sum(s => s.CurrentSubscriptions); /// public double IncomingKbps => ApiClients.OfType().Sum(s => s.IncomingKbps); + + /// + public new SocketExchangeOptions ClientOptions => (SocketExchangeOptions)base.ClientOptions; #endregion /// diff --git a/CryptoExchange.Net/Clients/CryptoRestClient.cs b/CryptoExchange.Net/Clients/CryptoRestClient.cs index d4ee0bb..1907661 100644 --- a/CryptoExchange.Net/Clients/CryptoRestClient.cs +++ b/CryptoExchange.Net/Clients/CryptoRestClient.cs @@ -1,8 +1,5 @@ -using CryptoExchange.Net.Interfaces; -using Microsoft.Extensions.DependencyInjection; +using CryptoExchange.Net.Interfaces.Clients; using System; -using System.Collections.Generic; -using System.Linq; namespace CryptoExchange.Net.Clients { diff --git a/CryptoExchange.Net/Clients/CryptoSocketClient.cs b/CryptoExchange.Net/Clients/CryptoSocketClient.cs index 5c33ef9..4e41c63 100644 --- a/CryptoExchange.Net/Clients/CryptoSocketClient.cs +++ b/CryptoExchange.Net/Clients/CryptoSocketClient.cs @@ -1,4 +1,4 @@ -using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Interfaces.Clients; using System; namespace CryptoExchange.Net.Clients diff --git a/CryptoExchange.Net/Clients/RestApiClient.cs b/CryptoExchange.Net/Clients/RestApiClient.cs index cb42254..8e26807 100644 --- a/CryptoExchange.Net/Clients/RestApiClient.cs +++ b/CryptoExchange.Net/Clients/RestApiClient.cs @@ -1,14 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; using CryptoExchange.Net.Caching; +using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters; using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Interfaces.Clients; using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Errors; @@ -17,6 +10,17 @@ using CryptoExchange.Net.RateLimiting; using CryptoExchange.Net.RateLimiting.Interfaces; using CryptoExchange.Net.Requests; using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; namespace CryptoExchange.Net.Clients { @@ -90,6 +94,11 @@ namespace CryptoExchange.Net.Clients /// private readonly static MemoryCache _cache = new MemoryCache(); + /// + /// The message handler + /// + protected abstract IRestMessageHandler MessageHandler { get; } + /// /// ctor /// @@ -204,6 +213,13 @@ namespace CryptoExchange.Net.Clients int? weightSingleLimiter = null, string? rateLimitKeySuffix = null) { + var requestId = ExchangeHelpers.NextId(); + if (definition.Authenticated && AuthenticationProvider == null) + { + _logger.RestApiNoApiCredentials(requestId, definition.Path); + return new WebCallResult(new NoApiCredentialsError()); + } + string? cacheKey = null; if (ShouldCache(definition)) { @@ -224,11 +240,21 @@ namespace CryptoExchange.Net.Clients while (true) { currentTry++; - var requestId = ExchangeHelpers.NextId(); - var prepareResult = await PrepareAsync(requestId, baseAddress, definition, cancellationToken, additionalHeaders, weight, weightSingleLimiter, rateLimitKeySuffix).ConfigureAwait(false); - if (!prepareResult) - return new WebCallResult(prepareResult.Error!); + var error = await CheckTimeSync(requestId, definition).ConfigureAwait(false); + if (error != null) + return new WebCallResult(error); + + error = await RateLimitAsync( + baseAddress, + requestId, + definition, + weight ?? definition.Weight, + cancellationToken, + weightSingleLimiter, + rateLimitKeySuffix).ConfigureAwait(false); + if (error != null) + return new WebCallResult(error); var request = CreateRequest( requestId, @@ -237,16 +263,24 @@ namespace CryptoExchange.Net.Clients uriParameters, bodyParameters, additionalHeaders); - _logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"))); + + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"))); TotalRequestsMade++; - var result = await GetResponseAsync(definition, request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false); + + var result = await GetResponseAsync2(definition, request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false); if (result.Error is not CancellationRequestedError) { var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]"; if (!result) + { _logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString(), originalData, result.Error?.Exception); + } else - _logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), originalData); + { + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), originalData); + } } else { @@ -266,55 +300,42 @@ namespace CryptoExchange.Net.Clients } } + private async ValueTask CheckTimeSync(int requestId, RequestDefinition definition) + { + if (!definition.Authenticated) + return null; + + var syncTask = SyncTimeAsync(); + var timeSyncInfo = GetTimeSyncInfo(); + + if (timeSyncInfo != null && timeSyncInfo.TimeSyncState.LastSyncTime == default) + { + // Initially with first request we'll need to wait for the time syncing, if it's not the first request we can just continue + var syncTimeError = await syncTask.ConfigureAwait(false); + if (syncTimeError != null) + { + _logger.RestApiFailedToSyncTime(requestId, syncTimeError!.ToString()); + return syncTimeError; + } + } + + return null; + } + /// - /// Prepare before sending a request. Sync time between client and server and check rate limits + /// Check rate limits for the request /// - /// Request id - /// Host and schema - /// Request definition - /// Cancellation token - /// Additional headers for this request - /// Override the request weight for this request - /// Specify the weight to apply to the individual rate limit guard for this request - /// An additional optional suffix for the key selector - /// - /// - protected virtual async Task PrepareAsync( + protected virtual async ValueTask RateLimitAsync( + string host, int requestId, - string baseAddress, RequestDefinition definition, + int weight, CancellationToken cancellationToken, - Dictionary? additionalHeaders = null, - int? weight = null, int? weightSingleLimiter = null, string? rateLimitKeySuffix = null) { - // Time sync - if (definition.Authenticated) - { - if (AuthenticationProvider == null) - { - _logger.RestApiNoApiCredentials(requestId, definition.Path); - return new CallResult(new NoApiCredentialsError()); - } - - var syncTask = SyncTimeAsync(); - var timeSyncInfo = GetTimeSyncInfo(); - - if (timeSyncInfo != null && timeSyncInfo.TimeSyncState.LastSyncTime == default) - { - // Initially with first request we'll need to wait for the time syncing, if it's not the first request we can just continue - var syncTimeResult = await syncTask.ConfigureAwait(false); - if (!syncTimeResult) - { - _logger.RestApiFailedToSyncTime(requestId, syncTimeResult.Error!.ToString()); - return syncTimeResult.AsDataless(); - } - } - } - // Rate limiting - var requestWeight = weight ?? definition.Weight; + var requestWeight = weight; if (requestWeight != 0) { if (definition.RateLimitGate == null) @@ -322,9 +343,9 @@ namespace CryptoExchange.Net.Clients if (ClientOptions.RateLimiterEnabled) { - var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false); + var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, host, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false); if (!limitResult) - return new CallResult(limitResult.Error!); + return limitResult.Error!; } } @@ -337,13 +358,13 @@ namespace CryptoExchange.Net.Clients if (ClientOptions.RateLimiterEnabled) { var singleRequestWeight = weightSingleLimiter ?? 1; - var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, singleRequestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false); + var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, host, AuthenticationProvider?._credentials.Key, singleRequestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false); if (!limitResult) - return new CallResult(limitResult.Error!); + return limitResult.Error!; } } - return CallResult.SuccessResult; + return null; } /// @@ -367,9 +388,9 @@ namespace CryptoExchange.Net.Clients var requestConfiguration = new RestRequestConfiguration( definition, baseAddress, - uriParameters == null ? new Dictionary() : CreateParameterDictionary(uriParameters), - bodyParameters == null ? new Dictionary() : CreateParameterDictionary(bodyParameters), - new Dictionary(additionalHeaders ?? []), + uriParameters == null ? null : CreateParameterDictionary(uriParameters), + bodyParameters == null ? null : CreateParameterDictionary(bodyParameters), + additionalHeaders, definition.ArraySerialization ?? ArraySerialization, definition.ParameterPosition ?? ParameterPositions[definition.Method], definition.RequestBodyFormat ?? RequestBodyFormat); @@ -389,14 +410,18 @@ namespace CryptoExchange.Net.Clients var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString); var request = RequestFactory.Create(ClientOptions.HttpVersion, definition.Method, uri, requestId); - request.Accept = Constants.JsonContentHeader; + request.Accept = MessageHandler.AcceptHeader; - foreach (var header in requestConfiguration.Headers) - request.AddHeader(header.Key, header.Value); + if (requestConfiguration.Headers != null) + { + foreach (var header in requestConfiguration.Headers) + request.AddHeader(header.Key, header.Value); + } foreach (var header in StandardRequestHeaders) { // Only add it if it isn't overwritten + requestConfiguration.Headers ??= new Dictionary(); if (!requestConfiguration.Headers.ContainsKey(header.Key)) request.AddHeader(header.Key, header.Value); } @@ -429,7 +454,7 @@ namespace CryptoExchange.Net.Clients /// The ratelimit gate used /// Cancellation token /// - protected virtual async Task> GetResponseAsync( + protected virtual async Task> GetResponseAsync2( RequestDefinition requestDefinition, IRequest request, IRateLimitGate? gate, @@ -438,24 +463,48 @@ namespace CryptoExchange.Net.Clients var sw = Stopwatch.StartNew(); Stream? responseStream = null; IResponse? response = null; - IStreamMessageAccessor? accessor = null; + try { response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false); sw.Stop(); responseStream = await response.GetResponseStreamAsync(cancellationToken).ConfigureAwait(false); + string? originalData = null; var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData; + if (outputOriginalData || MessageHandler.RequiresSeekableStream) + { + // If we want to return the original string data from the stream, but still want to process it + // we'll need to copy it as the stream isn't seekable, and thus we can only read it once + var memoryStream = new MemoryStream(); + await responseStream.CopyToAsync(memoryStream).ConfigureAwait(false); + using var reader = new StreamReader(memoryStream, Encoding.UTF8, false, 4096, true); + if (outputOriginalData) + { + memoryStream.Position = 0; + originalData = await reader.ReadToEndAsync().ConfigureAwait(false); + + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.RestApiReceivedResponse(request.RequestId, originalData); + } + + // Continue processing from the memory stream since the response stream is already read and we can't seek it + responseStream.Close(); + memoryStream.Position = 0; + responseStream = memoryStream; + } - accessor = CreateAccessor(); if (!response.IsSuccessStatusCode && !requestDefinition.TryParseOnNonSuccess) { - // Error response - var readResult = await accessor.Read(responseStream, true).ConfigureAwait(false); + // If the response status is not success it is an error by definition Error error; if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429) { - var rateError = ParseRateLimitResponse((int)response.StatusCode, response.ResponseHeaders, accessor); + // Specifically handle rate limit errors + var rateError = await MessageHandler.ParseErrorRateLimitResponse( + (int)response.StatusCode, + response.ResponseHeaders, + responseStream).ConfigureAwait(false); if (rateError.RetryAfter != null && gate != null && ClientOptions.RateLimiterEnabled) { _logger.RestApiRateLimitPauseUntil(request.RequestId, rateError.RetryAfter.Value); @@ -466,28 +515,25 @@ namespace CryptoExchange.Net.Clients } else { - error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor, readResult.Error?.Exception); + // Handle a 'normal' error response. Can still be either a json error message or some random HTML or other string + error = await MessageHandler.ParseErrorResponse( + (int)response.StatusCode, + response.ResponseHeaders, + responseStream).ConfigureAwait(false); } - if (error.Code == null || error.Code == 0) - error.Code = (int)response.StatusCode; - - return new WebCallResult(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!); + return new WebCallResult(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error); } - var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false); if (typeof(T) == typeof(object)) // Success status code and expected empty response, assume it's correct - return new WebCallResult(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, 0, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]", request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null); + return new WebCallResult(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, 0, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null); - if (!valid) - { - // Invalid json - return new WebCallResult(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, valid.Error); - } - - // Json response received - var parsedError = TryParseError(requestDefinition, response.ResponseHeaders, accessor); + // Data response received, inspect the message and check if it is an error or not + var parsedError = await MessageHandler.CheckForErrorResponse( + requestDefinition, + response.ResponseHeaders, + responseStream).ConfigureAwait(false); if (parsedError != null) { if (parsedError is ServerRateLimitError rateError) @@ -500,11 +546,24 @@ namespace CryptoExchange.Net.Clients } // Success status code, but TryParseError determined it was an error response - return new WebCallResult(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError); + return new WebCallResult(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError); } - var deserializeResult = accessor.Deserialize(); - return new WebCallResult(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error); + if (MessageHandler.RequiresSeekableStream) + // Reset stream read position as it might not be at the start if `CheckForErrorResponse` has read from it + responseStream.Position = 0; + + // Try deserialization into the expected type + var (deserializeResult, deserializeError) = await MessageHandler.TryDeserializeAsync(responseStream, cancellationToken).ConfigureAwait(false); + if (deserializeError != null) + return new WebCallResult(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, deserializeError); ; + + // Check the deserialized response to see if it's an error or not + var responseError = MessageHandler.CheckDeserializedResponse(response.ResponseHeaders, deserializeResult); + if (responseError != null) + return new WebCallResult(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, responseError); + + return new WebCallResult(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, null); } catch (HttpRequestException requestException) { @@ -551,23 +610,11 @@ namespace CryptoExchange.Net.Clients } finally { - accessor?.Clear(); responseStream?.Close(); response?.Close(); } } - /// - /// Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error. - /// This method will be called for each response to be able to check if the response is an error or not. - /// If the response is an error this method should return the parsed error, else it should return null - /// - /// Request definition - /// Data accessor - /// The response headers - /// Null if not an error, Error otherwise - protected virtual Error? TryParseError(RequestDefinition requestDefinition, KeyValuePair[] responseHeaders, IMessageAccessor accessor) => null; - /// /// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever. /// Note that this is always called; even when the request might be successful @@ -577,7 +624,7 @@ namespace CryptoExchange.Net.Clients /// The result of the call /// The current try number /// True if call should retry, false if the call should return - protected virtual async Task ShouldRetryRequestAsync(IRateLimitGate? gate, WebCallResult callResult, int tries) + protected virtual async ValueTask ShouldRetryRequestAsync(IRateLimitGate? gate, WebCallResult callResult, int tries) { if (tries >= 2) // Only retry once @@ -632,43 +679,6 @@ namespace CryptoExchange.Net.Clients } } - /// - /// Parse an error response from the server. Only used when server returns a status other than Success(200) or ratelimit error (429 or 418) - /// - /// The response status code - /// The response headers - /// Data accessor - /// Exception - /// - protected virtual Error ParseErrorResponse(int httpStatusCode, KeyValuePair[] responseHeaders, IMessageAccessor accessor, Exception? exception) - { - return new ServerError(ErrorInfo.Unknown, exception); - } - - /// - /// Parse a rate limit error response from the server. Only used when server returns http status 429 or 418 - /// - /// The response status code - /// The response headers - /// Data accessor - /// - protected virtual ServerRateLimitError ParseRateLimitResponse(int httpStatusCode, KeyValuePair[] responseHeaders, IMessageAccessor accessor) - { - // Handle retry after header - var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase)); - if (retryAfterHeader.Value?.Any() != true) - return new ServerRateLimitError(); - - var value = retryAfterHeader.Value.First(); - if (int.TryParse(value, out var seconds)) - return new ServerRateLimitError() { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) }; - - if (DateTime.TryParse(value, out var datetime)) - return new ServerRateLimitError() { RetryAfter = datetime }; - - return new ServerRateLimitError(); - } - /// /// Create the parameter IDictionary /// @@ -696,18 +706,18 @@ namespace CryptoExchange.Net.Clients RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout, ClientOptions.HttpKeepAliveInterval); } - internal async Task> SyncTimeAsync() + internal async ValueTask SyncTimeAsync() { var timeSyncParams = GetTimeSyncInfo(); if (timeSyncParams == null) - return new WebCallResult(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null); + return null; if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false)) { if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval) { timeSyncParams.TimeSyncState.Semaphore.Release(); - return new WebCallResult(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null); + return null; } var localTime = DateTime.UtcNow; @@ -715,7 +725,7 @@ namespace CryptoExchange.Net.Clients if (!result) { timeSyncParams.TimeSyncState.Semaphore.Release(); - return result.As(false); + return result.Error; } if (TotalRequestsMade == 1) @@ -726,7 +736,7 @@ namespace CryptoExchange.Net.Clients if (!result) { timeSyncParams.TimeSyncState.Semaphore.Release(); - return result.As(false); + return result.Error; } } @@ -736,12 +746,13 @@ namespace CryptoExchange.Net.Clients timeSyncParams.TimeSyncState.Semaphore.Release(); } - return new WebCallResult(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null); + return null; } private bool ShouldCache(RequestDefinition definition) => ClientOptions.CachingEnabled && definition.Method == HttpMethod.Get && !definition.PreventCaching; + } } diff --git a/CryptoExchange.Net/Clients/SocketApiClient.cs b/CryptoExchange.Net/Clients/SocketApiClient.cs index 1c0c3f3..aea4ff4 100644 --- a/CryptoExchange.Net/Clients/SocketApiClient.cs +++ b/CryptoExchange.Net/Clients/SocketApiClient.cs @@ -1,4 +1,6 @@ +using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters; using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Interfaces.Clients; using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Errors; @@ -7,6 +9,11 @@ using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.RateLimiting; using CryptoExchange.Net.RateLimiting.Interfaces; using CryptoExchange.Net.Sockets; +using CryptoExchange.Net.Sockets.Default; +using CryptoExchange.Net.Sockets.Default.Interfaces; +using CryptoExchange.Net.Sockets.HighPerf; +using CryptoExchange.Net.Sockets.HighPerf.Interfaces; +using CryptoExchange.Net.Sockets.Interfaces; using Microsoft.Extensions.Logging; using System; using System.Collections.Concurrent; @@ -27,11 +34,18 @@ namespace CryptoExchange.Net.Clients #region Fields /// public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory(); + /// + public IHighPerfConnectionFactory? HighPerfConnectionFactory { get; set; } /// /// List of socket connections currently connecting/connected /// - protected internal ConcurrentDictionary socketConnections = new(); + protected internal ConcurrentDictionary _socketConnections = new(); + + /// + /// List of HighPerf socket connections currently connecting/connected + /// + protected internal ConcurrentDictionary _highPerfSocketConnections = new(); /// /// Semaphore used while creating sockets @@ -72,7 +86,7 @@ namespace CryptoExchange.Net.Clients /// Periodic task registrations /// protected List PeriodicTaskRegistrations { get; set; } = new List(); - + /// /// List of address to keep an alive connection to /// @@ -93,25 +107,25 @@ namespace CryptoExchange.Net.Clients { get { - if (socketConnections.IsEmpty) + if (_socketConnections.IsEmpty) return 0; - return socketConnections.Sum(s => s.Value.IncomingKbps); + return _socketConnections.Sum(s => s.Value.IncomingKbps); } } /// - public int CurrentConnections => socketConnections.Count; + public int CurrentConnections => _socketConnections.Count; /// public int CurrentSubscriptions { get { - if (socketConnections.IsEmpty) + if (_socketConnections.IsEmpty) return 0; - return socketConnections.Sum(s => s.Value.UserSubscriptionCount); + return _socketConnections.Sum(s => s.Value.UserSubscriptionCount); } } @@ -121,6 +135,11 @@ namespace CryptoExchange.Net.Clients /// public new SocketApiOptions ApiOptions => (SocketApiOptions)base.ApiOptions; + /// + /// The max number of individual subscriptions on a single connection + /// + public int? MaxIndividualSubscriptionsPerConnection { get; set; } + #endregion /// @@ -169,7 +188,7 @@ namespace CryptoExchange.Net.Clients /// /// /// - protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func queryDelegate, Action? callback) + protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func queryDelegate, Action? callback) { PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration { @@ -209,6 +228,9 @@ namespace CryptoExchange.Net.Clients return new CallResult(new NoApiCredentialsError()); } + if (subscription.IndividualSubscriptionCount > MaxIndividualSubscriptionsPerConnection) + return new CallResult(ArgumentError.Invalid("subscriptions", $"Max number of subscriptions in a single call is {MaxIndividualSubscriptionsPerConnection}")); + SocketConnection socketConnection; var released = false; // Wait for a semaphore here, so we only connect 1 socket at a time. @@ -227,7 +249,7 @@ namespace CryptoExchange.Net.Clients while (true) { // Get a new or existing socket connection - var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic).ConfigureAwait(false); + var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic, subscription.IndividualSubscriptionCount).ConfigureAwait(false); if (!socketResult) return socketResult.As(null); @@ -269,16 +291,33 @@ namespace CryptoExchange.Net.Clients return new CallResult(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused"))); } + void HandleSubscriptionComplete(bool success, object? response) + { + if (!success) + return; + + subscription.HandleSubQueryResponse(response); + subscription.Status = SubscriptionStatus.Subscribed; + if (ct != default) + { + subscription.CancellationTokenRegistration = ct.Register(async () => + { + _logger.CancellationTokenSetClosingSubscription(socketConnection.SocketId, subscription.Id); + await socketConnection.CloseAsync(subscription).ConfigureAwait(false); + }, false); + } + } + subscription.Status = SubscriptionStatus.Subscribing; - var waitEvent = new AsyncResetEvent(false); var subQuery = subscription.CreateSubscriptionQuery(socketConnection); if (subQuery != null) { + subQuery.OnComplete = () => HandleSubscriptionComplete(subQuery.Result?.Success ?? false, subQuery.Response); + // Send the request and wait for answer - var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, waitEvent, ct).ConfigureAwait(false); + var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, ct).ConfigureAwait(false); if (!subResult) { - waitEvent?.Set(); var isTimeout = subResult.Error is CancellationRequestedError; if (isTimeout && subscription.Status == SubscriptionStatus.Subscribed) { @@ -287,29 +326,116 @@ namespace CryptoExchange.Net.Clients else { _logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString()); - // If this was a timeout we still need to send an unsubscribe to prevent messages coming in later + // If this was a server process error we still might need to send an unsubscribe to prevent messages coming in later subscription.Status = SubscriptionStatus.Pending; await socketConnection.CloseAsync(subscription).ConfigureAwait(false); return new CallResult(subResult.Error!); } } - - subscription.HandleSubQueryResponse(subQuery.Response!); + } + else + { + HandleSubscriptionComplete(true, null); + } + + _logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id); + return new CallResult(new UpdateSubscription(socketConnection, subscription)); + } + + /// + /// Connect to an url and listen for data + /// + /// The URL to connect to + /// The subscription + /// The factory for creating a socket connection + /// Cancellation token for closing this subscription + /// + protected virtual async Task> SubscribeHighPerfAsync( + string url, + HighPerfSubscription subscription, + IHighPerfConnectionFactory connectionFactory, + CancellationToken ct) + { + if (_disposing) + return new CallResult(new InvalidOperationError("Client disposed, can't subscribe")); + + HighPerfSocketConnection socketConnection; + var released = false; + // Wait for a semaphore here, so we only connect 1 socket at a time. + // This is necessary for being able to see if connections can be combined + try + { + await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false); + } + catch (OperationCanceledException tce) + { + return new CallResult(new CancellationRequestedError(tce)); + } + + try + { + while (true) + { + // Get a new or existing socket connection + var socketResult = await GetHighPerfSocketConnection(url, connectionFactory, ct).ConfigureAwait(false); + if (!socketResult) + return socketResult.As(null); + + socketConnection = socketResult.Data; + + // Add a subscription on the socket connection + var success = socketConnection.AddSubscription(subscription); + if (!success) + { + _logger.FailedToAddSubscriptionRetryOnDifferentConnection(socketConnection.SocketId); + continue; + } + + if (ClientOptions.SocketSubscriptionsCombineTarget == 1) + { + // Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway + semaphoreSlim.Release(); + released = true; + } + + var needsConnecting = !socketConnection.Connected; + + var connectResult = await ConnectIfNeededAsync(socketConnection, false, ct).ConfigureAwait(false); + if (!connectResult) + return new CallResult(connectResult.Error!); + + break; + } + } + finally + { + if (!released) + semaphoreSlim.Release(); + } + + var subRequest = subscription.CreateSubscriptionQuery(socketConnection); + if (subRequest != null) + { + // Send the request and wait for answer + var sendResult = await socketConnection.SendAsync(subRequest).ConfigureAwait(false); + if (!sendResult) + { + await socketConnection.CloseAsync().ConfigureAwait(false); + return new CallResult(sendResult.Error!); + } } - subscription.Status = SubscriptionStatus.Subscribed; if (ct != default) { subscription.CancellationTokenRegistration = ct.Register(async () => { _logger.CancellationTokenSetClosingSubscription(socketConnection.SocketId, subscription.Id); - await socketConnection.CloseAsync(subscription).ConfigureAwait(false); + await socketConnection.CloseAsync().ConfigureAwait(false); }, false); } - waitEvent?.Set(); _logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id); - return new CallResult(new UpdateSubscription(socketConnection, subscription)); + return new CallResult(new HighPerfUpdateSubscription(socketConnection, subscription)); } /// @@ -377,7 +503,7 @@ namespace CryptoExchange.Net.Clients if (ct.IsCancellationRequested) return new CallResult(new CancellationRequestedError()); - return await socketConnection.SendAndWaitQueryAsync(query, null, ct).ConfigureAwait(false); + return await socketConnection.SendAndWaitQueryAsync(query, ct).ConfigureAwait(false); } /// @@ -387,7 +513,7 @@ namespace CryptoExchange.Net.Clients /// Whether the socket should authenticated /// Cancellation token /// - protected virtual async Task ConnectIfNeededAsync(SocketConnection socket, bool authenticated, CancellationToken ct) + protected virtual async Task ConnectIfNeededAsync(ISocketConnection socket, bool authenticated, CancellationToken ct) { if (socket.Connected) return CallResult.SuccessResult; @@ -402,7 +528,10 @@ namespace CryptoExchange.Net.Clients if (!authenticated || socket.Authenticated) return CallResult.SuccessResult; - var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false); + if (socket is not SocketConnection sc) + throw new InvalidOperationException("HighPerfSocketConnection not supported for authentication"); + + var result = await AuthenticateSocketAsync(sc).ConfigureAwait(false); if (!result) await socket.CloseAsync().ConfigureAwait(false); @@ -455,7 +584,7 @@ namespace CryptoExchange.Net.Clients protected void AddSystemSubscription(SystemSubscription systemSubscription) { systemSubscriptions.Add(systemSubscription); - foreach (var connection in socketConnections.Values) + foreach (var connection in _socketConnections.Values) connection.AddSubscription(systemSubscription); } @@ -475,7 +604,7 @@ namespace CryptoExchange.Net.Clients /// /// /// - protected internal virtual Task GetReconnectUriAsync(SocketConnection connection) + protected internal virtual Task GetReconnectUriAsync(ISocketConnection connection) { return Task.FromResult(connection.ConnectionUri); } @@ -498,10 +627,17 @@ namespace CryptoExchange.Net.Clients /// Whether a dedicated request connection should be returned /// Cancellation token /// The subscription topic, can be provided when multiple of the same topics are not allowed on a connection + /// The number of individual subscriptions in this subscribe request /// - protected virtual async Task> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection, CancellationToken ct, string? topic = null) + protected virtual async Task> GetSocketConnection( + string address, + bool authenticated, + bool dedicatedRequestConnection, + CancellationToken ct, + string? topic = null, + int individualSubscriptionCount = 1) { - var socketQuery = socketConnections.Where(s => s.Value.Tag.TrimEnd('/') == address.TrimEnd('/') + var socketQuery = _socketConnections.Where(s => s.Value.Tag.TrimEnd('/') == address.TrimEnd('/') && s.Value.ApiClient.GetType() == GetType() && (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic))) .Select(x => x.Value) @@ -510,11 +646,11 @@ namespace CryptoExchange.Net.Clients // If all current socket connections are reconnecting or resubscribing wait for that to finish as we can probably use the existing connection var delayStart = DateTime.UtcNow; var delayed = false; - while (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketConnection.SocketStatus.Reconnecting || x.Status == SocketConnection.SocketStatus.Resubscribing)) + while (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing)) { if (DateTime.UtcNow - delayStart > TimeSpan.FromSeconds(10)) { - if (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketConnection.SocketStatus.Reconnecting || x.Status == SocketConnection.SocketStatus.Resubscribing)) + if (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing)) { // If after this time we still trying to reconnect/reprocess there is some issue in the connection _logger.TimeoutWaitingForReconnectingSocket(); @@ -534,7 +670,7 @@ namespace CryptoExchange.Net.Clients if (delayed) _logger.WaitedForReconnectingSocket((long)(DateTime.UtcNow - delayStart).TotalMilliseconds); - socketQuery = socketQuery.Where(s => (s.Status == SocketConnection.SocketStatus.None || s.Status == SocketConnection.SocketStatus.Connected) + socketQuery = socketQuery.Where(s => (s.Status == SocketStatus.None || s.Status == SocketStatus.Connected) && (s.Authenticated == authenticated || !authenticated) && s.Connected).ToList(); @@ -551,16 +687,29 @@ namespace CryptoExchange.Net.Clients connection.DedicatedRequestConnection.Authenticated = authenticated; } + bool maxConnectionsReached = _socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections); if (connection != null) { - if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget - || (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget))) + bool lessThanBatchSubCombineTarget = connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget; + bool lessThanIndividualSubCombineTarget = connection.Subscriptions.Sum(x => x.IndividualSubscriptionCount) < ClientOptions.SocketIndividualSubscriptionCombineTarget; + + if ((lessThanBatchSubCombineTarget && lessThanIndividualSubCombineTarget) + || maxConnectionsReached) { // Use existing socket if it has less than target connections OR it has the least connections and we can't make new - return new CallResult(connection); + // If there is a max subscriptions per connection limit also only use existing if the new subscription doesn't go over the limit + if (MaxIndividualSubscriptionsPerConnection == null) + return new CallResult(connection); + + var currentCount = connection.Subscriptions.Sum(x => x.IndividualSubscriptionCount); + if (currentCount + individualSubscriptionCount <= MaxIndividualSubscriptionsPerConnection) + return new CallResult(connection); } } + if (maxConnectionsReached) + return new CallResult(new InvalidOperationError("Max amount of socket connections reached")); + var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false); if (!connectionAddress) { @@ -571,9 +720,8 @@ namespace CryptoExchange.Net.Clients if (connectionAddress.Data != address) _logger.ConnectionAddressSetTo(connectionAddress.Data!); - // Create new socket - var socket = CreateSocket(connectionAddress.Data!); - var socketConnection = new SocketConnection(_logger, this, socket, address); + // Create new socket connection + var socketConnection = new SocketConnection(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this, address); socketConnection.UnhandledMessage += HandleUnhandledMessage; socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync; if (dedicatedRequestConnection) @@ -594,6 +742,38 @@ namespace CryptoExchange.Net.Clients return new CallResult(socketConnection); } + + /// + /// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one. + /// + /// The address the socket is for + /// The factory for creating a socket connection + /// Cancellation token + /// + protected virtual async Task>> GetHighPerfSocketConnection( + string address, + IHighPerfConnectionFactory connectionFactory, + CancellationToken ct) + { + var connectionAddress = await GetConnectionUrlAsync(address, false).ConfigureAwait(false); + if (!connectionAddress) + { + _logger.FailedToDetermineConnectionUrl(connectionAddress.Error?.ToString()); + return connectionAddress.As>(null); + } + + if (connectionAddress.Data != address) + _logger.ConnectionAddressSetTo(connectionAddress.Data!); + + // Create new socket connection + var socketConnection = connectionFactory.CreateHighPerfConnection(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this, address); + foreach (var ptg in PeriodicTaskRegistrations) + socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, (con) => ptg.QueryDelegate(con).Request); + + return new CallResult>(socketConnection); + } + + /// /// Process an unhandled message /// @@ -622,12 +802,15 @@ namespace CryptoExchange.Net.Clients /// The socket to connect /// Cancellation token /// - protected virtual async Task ConnectSocketAsync(SocketConnection socketConnection, CancellationToken ct) + protected virtual async Task ConnectSocketAsync(ISocketConnection socketConnection, CancellationToken ct) { var connectResult = await socketConnection.ConnectAsync(ct).ConfigureAwait(false); if (connectResult) { - socketConnections.TryAdd(socketConnection.SocketId, socketConnection); + if (socketConnection is SocketConnection sc) + _socketConnections.TryAdd(socketConnection.SocketId, sc); + else if (socketConnection is HighPerfSocketConnection hsc) + _highPerfSocketConnections.TryAdd(socketConnection.SocketId, hsc); return connectResult; } @@ -651,20 +834,9 @@ namespace CryptoExchange.Net.Clients Proxy = ClientOptions.Proxy, Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout, ReceiveBufferSize = ClientOptions.ReceiveBufferSize, + UseUpdatedDeserialization = ClientOptions.UseUpdatedDeserialization }; - /// - /// Create a socket for an address - /// - /// The address the socket should connect to - /// - protected virtual IWebsocket CreateSocket(string address) - { - var socket = SocketFactory.CreateWebsocket(_logger, GetWebSocketParameters(address)); - _logger.SocketCreatedForAddress(socket.Id, address); - return socket; - } - /// /// Unsubscribe an update subscription /// @@ -674,7 +846,7 @@ namespace CryptoExchange.Net.Clients { Subscription? subscription = null; SocketConnection? connection = null; - foreach (var socket in socketConnections.Values.ToList()) + foreach (var socket in _socketConnections.Values.ToList()) { subscription = socket.GetSubscription(subscriptionId); if (subscription != null) @@ -712,21 +884,25 @@ namespace CryptoExchange.Net.Clients /// public virtual async Task UnsubscribeAllAsync() { - var sum = socketConnections.Sum(s => s.Value.UserSubscriptionCount); + var sum = _socketConnections.Sum(s => s.Value.UserSubscriptionCount) + _highPerfSocketConnections.Sum(s => s.Value.UserSubscriptionCount); if (sum == 0) return; - _logger.UnsubscribingAll(socketConnections.Sum(s => s.Value.UserSubscriptionCount)); + _logger.UnsubscribingAll(sum); var tasks = new List(); + + var socketList = _socketConnections.Values; + foreach (var connection in socketList) { - var socketList = socketConnections.Values; - foreach (var connection in socketList) - { - foreach(var subscription in connection.Subscriptions.Where(x => x.UserSubscription)) - tasks.Add(connection.CloseAsync(subscription)); - } + foreach(var subscription in connection.Subscriptions.Where(x => x.UserSubscription)) + tasks.Add(connection.CloseAsync(subscription)); } + var highPerfSocketList = _highPerfSocketConnections.Values; + foreach (var connection in highPerfSocketList) + tasks.Add(connection.CloseAsync()); + + await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false); } @@ -736,10 +912,10 @@ namespace CryptoExchange.Net.Clients /// public virtual async Task ReconnectAsync() { - _logger.ReconnectingAllConnections(socketConnections.Count); + _logger.ReconnectingAllConnections(_socketConnections.Count); var tasks = new List(); { - var socketList = socketConnections.Values; + var socketList = _socketConnections.Values; foreach (var sub in socketList) tasks.Add(sub.TriggerReconnectAsync()); } @@ -771,7 +947,7 @@ namespace CryptoExchange.Net.Clients base.SetOptions(options); if ((!previousProxyIsSet && options.Proxy == null) - || socketConnections.IsEmpty) + || _socketConnections.IsEmpty) { return; } @@ -779,7 +955,7 @@ namespace CryptoExchange.Net.Clients _logger.LogInformation("Reconnecting websockets to apply proxy"); // Update proxy, also triggers reconnect - foreach (var connection in socketConnections) + foreach (var connection in _socketConnections) _ = connection.Value.UpdateProxy(options.Proxy); } @@ -798,15 +974,15 @@ namespace CryptoExchange.Net.Clients /// public SocketApiClientState GetState(bool includeSubDetails = true) { - var connectionStates = new List(); - foreach (var socketIdAndConnection in socketConnections) + var connectionStates = new List(); + foreach (var socketIdAndConnection in _socketConnections) { SocketConnection connection = socketIdAndConnection.Value; - SocketConnection.SocketConnectionState connectionState = connection.GetState(includeSubDetails); + SocketConnectionState connectionState = connection.GetState(includeSubDetails); connectionStates.Add(connectionState); } - return new SocketApiClientState(socketConnections.Count, CurrentSubscriptions, IncomingKbps, connectionStates); + return new SocketApiClientState(_socketConnections.Count, CurrentSubscriptions, IncomingKbps, connectionStates); } /// @@ -820,7 +996,7 @@ namespace CryptoExchange.Net.Clients int Connections, int Subscriptions, double DownloadSpeed, - List ConnectionStates) + List ConnectionStates) { /// /// Print the state of the client @@ -868,7 +1044,7 @@ namespace CryptoExchange.Net.Clients _disposing = true; var tasks = new List(); { - var socketList = socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected); + var socketList = _socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected); if (socketList.Any()) _logger.DisposingSocketClient(); @@ -892,10 +1068,16 @@ namespace CryptoExchange.Net.Clients /// /// Preprocess a stream message /// - /// - /// - /// - /// + public virtual ReadOnlySpan PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlySpan data) => data; + /// + /// Preprocess a stream message + /// public virtual ReadOnlyMemory PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlyMemory data) => data; + + /// + /// Create a new message converter instance + /// + /// + public abstract ISocketMessageHandler CreateMessageConverter(WebSocketMessageType messageType); } } diff --git a/CryptoExchange.Net/Converters/JsonSerializerContextCache.cs b/CryptoExchange.Net/Converters/JsonSerializerContextCache.cs index 55cb48f..46473c1 100644 --- a/CryptoExchange.Net/Converters/JsonSerializerContextCache.cs +++ b/CryptoExchange.Net/Converters/JsonSerializerContextCache.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Text; using System.Text.Json.Serialization; namespace CryptoExchange.Net.Converters diff --git a/CryptoExchange.Net/Converters/MessageParsing/DynamicConverters/IRestMessageHandler.cs b/CryptoExchange.Net/Converters/MessageParsing/DynamicConverters/IRestMessageHandler.cs new file mode 100644 index 0000000..2152a67 --- /dev/null +++ b/CryptoExchange.Net/Converters/MessageParsing/DynamicConverters/IRestMessageHandler.cs @@ -0,0 +1,64 @@ +using CryptoExchange.Net.Objects; +using System.IO; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; + +namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters +{ + /// + /// REST message handler + /// + public interface IRestMessageHandler + { + /// + /// The `accept` HTTP response header for the request + /// + MediaTypeWithQualityHeaderValue AcceptHeader { get; } + + /// + /// Whether a seekable stream is required + /// + bool RequiresSeekableStream { get; } + + /// + /// Parse the response when the HTTP response status indicated an error + /// + ValueTask ParseErrorResponse( + int httpStatusCode, + HttpResponseHeaders responseHeaders, + Stream responseStream); + + /// + /// Parse the response when the HTTP response status indicated a rate limit error + /// + ValueTask ParseErrorRateLimitResponse( + int httpStatusCode, + HttpResponseHeaders responseHeaders, + Stream responseStream); + + /// + /// Check if the response is an error response; if so return the error.
+ /// Note that if the API returns a standard result wrapper, something like this: + /// { "code": 400, "msg": "error", "data": {} } + /// then the `CheckDeserializedResponse` method should be used for checking the result + ///
+ ValueTask CheckForErrorResponse( + RequestDefinition request, + HttpResponseHeaders responseHeaders, + Stream responseStream); + + /// + /// Deserialize the response stream + /// + ValueTask<(T? Result, Error? Error)> TryDeserializeAsync( + Stream responseStream, + CancellationToken ct); + + /// + /// Check whether the resulting T object indicates an error or not + /// + Error? CheckDeserializedResponse(HttpResponseHeaders responseHeaders, T result); + } + +} diff --git a/CryptoExchange.Net/Converters/MessageParsing/DynamicConverters/ISocketMessageHandler.cs b/CryptoExchange.Net/Converters/MessageParsing/DynamicConverters/ISocketMessageHandler.cs new file mode 100644 index 0000000..00bd1d6 --- /dev/null +++ b/CryptoExchange.Net/Converters/MessageParsing/DynamicConverters/ISocketMessageHandler.cs @@ -0,0 +1,27 @@ +using System; +using System.Net.WebSockets; + +namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters +{ + /// + /// WebSocket message handler + /// + public interface ISocketMessageHandler + { + /// + /// Get an identifier for the message which can be used to determine the type of the message + /// + string? GetTypeIdentifier(ReadOnlySpan data, WebSocketMessageType? webSocketMessageType); + + /// + /// Get optional topic filter, for example a symbol name + /// + string? GetTopicFilter(object deserializedObject); + + /// + /// Deserialize to the provided type + /// + object Deserialize(ReadOnlySpan data, Type type); + } + +} diff --git a/CryptoExchange.Net/Converters/MessageParsing/DynamicConverters/MessageConverterTypes.cs b/CryptoExchange.Net/Converters/MessageParsing/DynamicConverters/MessageConverterTypes.cs new file mode 100644 index 0000000..5314aae --- /dev/null +++ b/CryptoExchange.Net/Converters/MessageParsing/DynamicConverters/MessageConverterTypes.cs @@ -0,0 +1,46 @@ +using System; + +namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters +{ + /// + /// Message type definition + /// + public class MessageTypeDefinition + { + /// + /// 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 + /// + public bool ForceIfFound { get; set; } + /// + /// The fields a message needs to contain for this definition + /// + public MessageFieldReference[] Fields { get; set; } = []; + /// + /// The callback for getting the identifier string + /// + public Func? TypeIdentifierCallback { get; set; } + /// + /// The static identifier string to return when this evaluator is matched + /// + 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; + } + } +} diff --git a/CryptoExchange.Net/Converters/MessageParsing/DynamicConverters/MessageEvalutorFieldReference.cs b/CryptoExchange.Net/Converters/MessageParsing/DynamicConverters/MessageEvalutorFieldReference.cs new file mode 100644 index 0000000..df93ba7 --- /dev/null +++ b/CryptoExchange.Net/Converters/MessageParsing/DynamicConverters/MessageEvalutorFieldReference.cs @@ -0,0 +1,15 @@ +namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters +{ + internal class MessageEvalutorFieldReference + { + public bool SkipReading { get; set; } + public bool OverlappingField { get; set; } + public MessageFieldReference Field { get; set; } + public MessageTypeDefinition? ForceEvaluator { get; set; } + + public MessageEvalutorFieldReference(MessageFieldReference field) + { + Field = field; + } + } +} diff --git a/CryptoExchange.Net/Converters/MessageParsing/DynamicConverters/MessageFieldReference.cs b/CryptoExchange.Net/Converters/MessageParsing/DynamicConverters/MessageFieldReference.cs new file mode 100644 index 0000000..5e34ba3 --- /dev/null +++ b/CryptoExchange.Net/Converters/MessageParsing/DynamicConverters/MessageFieldReference.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters +{ + /// + /// Reference to a message field + /// + public abstract class MessageFieldReference + { + /// + /// The name for this search field + /// + public string SearchName { get; set; } + /// + /// The depth at which to look for this field + /// + public int Depth { get; set; } = 1; + /// + /// Callback to check if the field value matches an expected constraint + /// + public Func? Constraint { get; private set; } + + /// + /// Check whether the value is one of the string values in the set + /// + public MessageFieldReference WithFilterConstraint(HashSet set) + { + Constraint = set.Contains; + return this; + } + + /// + /// Check whether the value is equal to a string + /// + public MessageFieldReference WithEqualConstraint(string compare) + { + Constraint = x => x != null && x.Equals(compare, StringComparison.Ordinal); + return this; + } + + /// + /// Check whether the value is not equal to a string + /// + public MessageFieldReference WithNotEqualConstraint(string compare) + { + Constraint = x => x == null || !x.Equals(compare, StringComparison.Ordinal); + return this; + } + + /// + /// Check whether the value is not null + /// + public MessageFieldReference WithNotNullConstraint() + { + Constraint = x => x != null; + return this; + } + + /// + /// Check whether the value starts with a certain string + /// + public MessageFieldReference WithStartsWithConstraint(string start) + { + Constraint = x => x != null && x.StartsWith(start, StringComparison.Ordinal); + return this; + } + + /// + /// Check whether the value starts with a certain string + /// + 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; + } + + /// + /// Check whether the value starts with a certain string + /// + public MessageFieldReference WithCustomConstraint(Func constraint) + { + Constraint = constraint; + return this; + } + + /// + /// ctor + /// + public MessageFieldReference(string searchName) + { + SearchName = searchName; + } + } + + /// + /// Reference to a property message field + /// + public class PropertyFieldReference : MessageFieldReference + { + /// + /// The property name in the JSON + /// + public byte[] PropertyName { get; set; } + /// + /// Whether the property value is array values + /// + public bool ArrayValues { get; set; } + + /// + /// ctor + /// + public PropertyFieldReference(string propertyName) : base(propertyName) + { + PropertyName = Encoding.UTF8.GetBytes(propertyName); + } + } + + /// + /// Reference to an array message field + /// + public class ArrayFieldReference : MessageFieldReference + { + /// + /// The index in the array + /// + public int ArrayIndex { get; set; } + + /// + /// ctor + /// + public ArrayFieldReference(string searchName, int depth, int index) : base(searchName) + { + Depth = depth; + ArrayIndex = index; + } + } + +} diff --git a/CryptoExchange.Net/Converters/MessageParsing/DynamicConverters/SearchResult.cs b/CryptoExchange.Net/Converters/MessageParsing/DynamicConverters/SearchResult.cs new file mode 100644 index 0000000..c9da198 --- /dev/null +++ b/CryptoExchange.Net/Converters/MessageParsing/DynamicConverters/SearchResult.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; + +namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters +{ + /// + /// The results of a search for fields in a JSON message + /// + public class SearchResult + { + private List _items = new List(); + + /// + /// Get the value of a field + /// + 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}"); + } + + /// + /// The number of found search field values + /// + public int Count => _items.Count; + + /// + /// Clear the search result + /// + public void Clear() => _items.Clear(); + + /// + /// Whether the value for a specific field was found + /// + public bool Contains(MessageFieldReference field) + { + foreach (var item in _items) + { + if (item.Field == field) + return true; + } + + return false; + } + + /// + /// Write a value to the result + /// + public void Write(MessageFieldReference field, string? value) => _items.Add(new SearchResultItem + { + Field = field, + Value = value + }); + } +} diff --git a/CryptoExchange.Net/Converters/MessageParsing/DynamicConverters/SearchResultItem.cs b/CryptoExchange.Net/Converters/MessageParsing/DynamicConverters/SearchResultItem.cs new file mode 100644 index 0000000..bd9c4d5 --- /dev/null +++ b/CryptoExchange.Net/Converters/MessageParsing/DynamicConverters/SearchResultItem.cs @@ -0,0 +1,17 @@ +namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters +{ + /// + /// Search result value + /// + public struct SearchResultItem + { + /// + /// The field the values is for + /// + public MessageFieldReference Field { get; set; } + /// + /// The value of the field + /// + public string? Value { get; set; } + } +} diff --git a/CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs index c378ebc..74f4915 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs @@ -1,15 +1,13 @@ -using System; -using System.Collections.Concurrent; +using CryptoExchange.Net.Exceptions; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Reflection; -using System.Text.Json.Serialization; using System.Text.Json; -using CryptoExchange.Net.Attributes; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using System.Threading; -using System.Diagnostics; namespace CryptoExchange.Net.Converters.SystemTextJson { @@ -23,8 +21,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson public class ArrayConverter : JsonConverter where T : new() #endif { - private static readonly Lazy> _typePropertyInfo = new Lazy>(CacheTypeAttributes, LazyThreadSafetyMode.PublicationOnly); - + private static SortedDictionary>? _typePropertyInfo; + /// #if NET5_0_OR_GREATER [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] @@ -38,54 +36,59 @@ namespace CryptoExchange.Net.Converters.SystemTextJson return; } + if (_typePropertyInfo == null) + _typePropertyInfo = CacheTypeAttributes(); + writer.WriteStartArray(); - - var ordered = _typePropertyInfo.Value.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index); var last = -1; - foreach (var prop in ordered) + foreach (var indexProps in _typePropertyInfo) { - if (prop.ArrayProperty.Index == last) - continue; - - while (prop.ArrayProperty.Index != last + 1) + foreach (var prop in indexProps.Value) { - writer.WriteNullValue(); - last += 1; - } + if (prop.ArrayProperty.Index == last) + // Don't write the same index twice + continue; - 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 + while (prop.ArrayProperty.Index != last + 1) { - NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals, - PropertyNameCaseInsensitive = false, - TypeInfoResolver = options.TypeInfoResolver, - }; - typeOptions.Converters.Add(prop.JsonConverter); - } + writer.WriteNullValue(); + 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); + last = prop.ArrayProperty.Index; + + var objValue = prop.PropertyInfo.GetValue(value); + if (objValue == null) + { + writer.WriteNullValue(); + continue; + } + + JsonSerializerOptions? typeOptions = null; + if (prop.JsonConverter != null) + { + typeOptions = new JsonSerializerOptions + { + NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals, + PropertyNameCaseInsensitive = false, + TypeInfoResolver = options.TypeInfoResolver, + }; + typeOptions.Converters.Add(prop.JsonConverter); + } + + if (prop.JsonConverter == null && IsSimple(prop.PropertyInfo.PropertyType)) + { + if (prop.TargetType == typeof(string)) + writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)); + else if (prop.TargetType == typeof(bool)) + writer.WriteBooleanValue((bool)objValue); + else + writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!); + } else - writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!); - } - else - { - JsonSerializer.Serialize(writer, objValue, typeOptions ?? options); + { + JsonSerializer.Serialize(writer, objValue, typeOptions ?? options); + } } } @@ -112,7 +115,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson #endif { if (reader.TokenType != JsonTokenType.StartArray) - throw new Exception("Not an array"); + throw new CeDeserializationException("Not an array"); + + + if (_typePropertyInfo == null) + _typePropertyInfo = CacheTypeAttributes(); int index = 0; while (reader.Read()) @@ -120,8 +127,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson if (reader.TokenType == JsonTokenType.EndArray) break; - var indexAttributes = _typePropertyInfo.Value.Where(a => a.ArrayProperty.Index == index); - if (!indexAttributes.Any()) + if(!_typePropertyInfo.TryGetValue(index, out var indexAttributes)) { index++; continue; @@ -161,7 +167,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson 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"), + _ => throw new CeDeserializationException($"Array deserialization of type {reader.TokenType} not supported"), }; } @@ -193,12 +199,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson #if NET5_0_OR_GREATER [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] - private static List CacheTypeAttributes() + private static SortedDictionary> CacheTypeAttributes() #else - private static List CacheTypeAttributes() + private static SortedDictionary> CacheTypeAttributes() #endif { - var attributes = new List(); + var result = new SortedDictionary>(); var properties = typeof(T).GetProperties(); foreach (var property in properties) { @@ -208,7 +214,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; var converterType = property.GetCustomAttribute()?.ConverterType ?? targetType.GetCustomAttribute()?.ConverterType; - attributes.Add(new ArrayPropertyInfo + if (!result.TryGetValue(att.Index, out var indexList)) + { + indexList = new List(); + result[att.Index] = indexList; + } + + indexList.Add(new ArrayPropertyInfo { ArrayProperty = att, PropertyInfo = property, @@ -218,7 +230,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson }); } - return attributes; + return result; } private class ArrayPropertyInfo diff --git a/CryptoExchange.Net/Converters/SystemTextJson/BoolConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/BoolConverter.cs index c996198..775ddde 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/BoolConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/BoolConverter.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using System; -using System.Diagnostics; using System.Runtime.Serialization; using System.Text.Json; using System.Text.Json.Serialization; @@ -21,58 +20,15 @@ namespace CryptoExchange.Net.Converters.SystemTextJson /// public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return typeToConvert == typeof(bool) ? new BoolConverterInner() : new BoolConverterInner(); + return typeToConvert == typeof(bool) ? new BoolConverterInner() : new BoolConverterInnerNullable(); } - private class BoolConverterInner : JsonConverter + private class BoolConverterInnerNullable : JsonConverter { - public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => (T)((object?)ReadBool(ref reader, typeToConvert, options) ?? default(T))!; + public override bool? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => ReadBool(ref reader, typeToConvert, options); - public bool? ReadBool(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.True) - return true; - - if (reader.TokenType == JsonTokenType.False) - return false; - - var value = reader.TokenType switch - { - JsonTokenType.String => reader.GetString(), - JsonTokenType.Number => reader.GetInt16().ToString(), - _ => null - }; - - value = value?.ToLowerInvariant().Trim(); - if (string.IsNullOrEmpty(value)) - { - if (typeToConvert == typeof(bool)) - LibraryHelpers.StaticLogger?.LogWarning("Received null bool value, but property type is not a nullable bool. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name); - return default; - } - - switch (value) - { - case "true": - case "yes": - case "y": - case "1": - case "on": - return true; - case "false": - case "no": - case "n": - case "0": - case "off": - case "-1": - return false; - } - - throw new SerializationException($"Can't convert bool value {value}"); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, bool? value, JsonSerializerOptions options) { if (value is bool boolVal) writer.WriteBooleanValue(boolVal); @@ -81,5 +37,59 @@ namespace CryptoExchange.Net.Converters.SystemTextJson } } + private class BoolConverterInner : JsonConverter + { + public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => ReadBool(ref reader, typeToConvert, options) ?? false; + + public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) + { + writer.WriteBooleanValue(value); + } + } + + private static bool? ReadBool(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.True) + return true; + + if (reader.TokenType == JsonTokenType.False) + return false; + + var value = reader.TokenType switch + { + JsonTokenType.String => reader.GetString(), + JsonTokenType.Number => reader.GetInt16().ToString(), + _ => null + }; + + value = value?.ToLowerInvariant().Trim(); + if (string.IsNullOrEmpty(value)) + { + if (typeToConvert == typeof(bool)) + LibraryHelpers.StaticLogger?.LogWarning("Received null or empty bool value, but property type is not a nullable bool. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name); + return default; + } + + switch (value) + { + case "true": + case "yes": + case "y": + case "1": + case "on": + return true; + case "false": + case "no": + case "n": + case "0": + case "off": + case "-1": + return false; + } + + throw new SerializationException($"Can't convert bool value {value}"); + } + } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/CommaSplitEnumConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/CommaSplitEnumConverter.cs index c327079..1bdc623 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/CommaSplitEnumConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/CommaSplitEnumConverter.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Text; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs index 2bd3a7b..357c02e 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using System; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text.Json; @@ -27,64 +26,77 @@ namespace CryptoExchange.Net.Converters.SystemTextJson /// public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner() : new DateTimeConverterInner(); + return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner() : new NullableDateTimeConverterInner(); } - private class DateTimeConverterInner : JsonConverter + private class NullableDateTimeConverterInner : JsonConverter { - public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => (T)((object?)ReadDateTime(ref reader, typeToConvert, options) ?? default(T))!; + public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => ReadDateTime(ref reader, typeToConvert, options); - private DateTime? ReadDateTime(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.Null) - { - if (typeToConvert == typeof(DateTime)) - LibraryHelpers.StaticLogger?.LogWarning("DateTime value of null, but property is not nullable. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name); - return default; - } - - if (reader.TokenType is JsonTokenType.Number) - { - var decValue = reader.GetDecimal(); - if (decValue == 0 || decValue < 0) - return default; - - return ParseFromDecimal(decValue); - } - else if (reader.TokenType is JsonTokenType.String) - { - var stringValue = reader.GetString(); - if (string.IsNullOrWhiteSpace(stringValue) - || stringValue == "-1" - || stringValue == "0001-01-01T00:00:00Z" - || decimal.TryParse(stringValue, out var decVal) && decVal == 0) - { - return default; - } - - return ParseFromString(stringValue!, options.TypeInfoResolver?.GetType()?.Name); - } - else - { - return reader.GetDateTime(); - } - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions 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 + { + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => ReadDateTime(ref reader, typeToConvert, options) ?? default; + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + { + var dtValue = value; + if (dtValue == default) + writer.WriteStringValue(default(DateTime)); + else + writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds)); + } + } + + private static DateTime? ReadDateTime(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + if (typeToConvert == typeof(DateTime)) + LibraryHelpers.StaticLogger?.LogWarning("DateTime value of null, but property is not nullable. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name); + return default; + } + + if (reader.TokenType is JsonTokenType.Number) + { + var decValue = reader.GetDecimal(); + if (decValue == 0 || decValue < 0) + return default; + + return ParseFromDecimal(decValue); + } + else if (reader.TokenType is JsonTokenType.String) + { + var stringValue = reader.GetString(); + if (string.IsNullOrWhiteSpace(stringValue) + || stringValue!.Equals("-1", StringComparison.Ordinal) + || stringValue!.Equals("0001-01-01T00:00:00Z", StringComparison.OrdinalIgnoreCase) + || decimal.TryParse(stringValue, out var decVal) && decVal == 0) { - var dtValue = (DateTime)(object)value; - if (dtValue == default) - writer.WriteStringValue(default(DateTime)); - else - writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds)); + return default; } + + return ParseFromString(stringValue!, options.TypeInfoResolver?.GetType()?.Name); + } + else + { + return reader.GetDateTime(); } } @@ -114,7 +126,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson ///
public static DateTime ParseFromString(string stringValue, string? resolverName) { - if (stringValue!.Length == 12 && stringValue.StartsWith("202")) + if (stringValue!.Length == 12 && stringValue.StartsWith("202", StringComparison.OrdinalIgnoreCase)) { // Parse 202303261200 format if (!int.TryParse(stringValue.Substring(0, 4), out var year) diff --git a/CryptoExchange.Net/Converters/SystemTextJson/DecimalConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/DecimalConverter.cs index 5d7b694..66c1ddf 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/DecimalConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/DecimalConverter.cs @@ -1,5 +1,4 @@ using System; -using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs index 4f1ab08..4f733ba 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs @@ -1,10 +1,11 @@ using CryptoExchange.Net.Attributes; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using System; using System.Collections.Concurrent; +#if NET8_0_OR_GREATER +using System.Collections.Frozen; +#endif using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; @@ -66,7 +67,25 @@ namespace CryptoExchange.Net.Converters.SystemTextJson #endif : JsonConverter, INullableConverterFactory where T : struct, Enum { - private static List>? _mapping = null; + class EnumMapping + { + public T Value { get; set; } + public string StringValue { get; set; } + + public EnumMapping(T value, string stringValue) + { + Value = value; + StringValue = stringValue; + } + } + +#if NET8_0_OR_GREATER + private static FrozenSet? _mappingToEnum = null; + private static FrozenDictionary? _mappingToString = null; +#else + private static List? _mappingToEnum = null; + private static Dictionary? _mappingToString = null; +#endif private NullableEnumConverter? _nullableEnumConverter = null; private static ConcurrentBag _unknownValuesWarned = new ConcurrentBag(); @@ -121,8 +140,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson { isEmptyString = false; var enumType = typeof(T); - if (_mapping == null) - _mapping = AddMapping(); + if (_mappingToEnum == null) + CreateMapping(); var stringValue = reader.TokenType switch { @@ -149,7 +168,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson if (!_unknownValuesWarned.Contains(stringValue)) { _unknownValuesWarned.Add(stringValue!); - LibraryHelpers.StaticLogger?.LogWarning($"Cannot map enum value. EnumType: {enumType.FullName}, 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"); + 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"); } } @@ -168,16 +187,35 @@ namespace CryptoExchange.Net.Converters.SystemTextJson private static bool GetValue(Type objectType, string value, out T? result) { - if (_mapping != null) + if (_mappingToEnum != 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))) - mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase)); - - if (!mapping.Equals(default(KeyValuePair))) + EnumMapping? mapping = null; + // Try match on full equals + foreach (var item in _mappingToEnum) { - result = mapping.Key; + if (item.StringValue.Equals(value, StringComparison.Ordinal)) + { + mapping = item; + break; + } + } + + // If not found, try matching ignoring case + if (mapping == null) + { + foreach (var item in _mappingToEnum) + { + if (item.StringValue.Equals(value, StringComparison.OrdinalIgnoreCase)) + { + mapping = item; + break; + } + } + } + + if (mapping != null) + { + result = mapping.Value; return true; } } @@ -217,9 +255,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson } } - private static List> AddMapping() + private static void CreateMapping() { - var mapping = new List>(); + var mappingToEnum = new List(); + var mappingToString = new Dictionary(); + var enumType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); var enumMembers = enumType.GetFields(); foreach (var member in enumMembers) @@ -228,12 +268,22 @@ namespace CryptoExchange.Net.Converters.SystemTextJson foreach (MapAttribute attribute in maps) { foreach (var value in attribute.Values) - mapping.Add(new KeyValuePair((T)Enum.Parse(enumType, member.Name), value)); + { + var enumVal = (T)Enum.Parse(enumType, member.Name); + mappingToEnum.Add(new EnumMapping(enumVal, value)); + if (!mappingToString.ContainsKey(enumVal)) + mappingToString.Add(enumVal, value); + } } } - _mapping = mapping; - return mapping; +#if NET8_0_OR_GREATER + _mappingToEnum = mappingToEnum.ToFrozenSet(); + _mappingToString = mappingToString.ToFrozenDictionary(); +#else + _mappingToEnum = mappingToEnum; + _mappingToString = mappingToString; +#endif } /// @@ -244,10 +294,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson [return: NotNullIfNotNull("enumValue")] public static string? GetString(T? enumValue) { - if (_mapping == null) - _mapping = AddMapping(); + if (_mappingToString == null) + CreateMapping(); - return enumValue == null ? null : (_mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString()); + return enumValue == null ? null : (_mappingToString!.TryGetValue(enumValue.Value, out var str) ? str : enumValue.ToString()); } /// @@ -258,15 +308,35 @@ namespace CryptoExchange.Net.Converters.SystemTextJson public static T? ParseString(string value) { var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); - if (_mapping == null) - _mapping = AddMapping(); + if (_mappingToEnum == null) + CreateMapping(); - var mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture)); - if (mapping.Equals(default(KeyValuePair))) - mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase)); + EnumMapping? mapping = null; + // Try match on full equals + foreach(var item in _mappingToEnum!) + { + if (item.StringValue.Equals(value, StringComparison.Ordinal)) + { + mapping = item; + break; + } + } - if (!mapping.Equals(default(KeyValuePair))) - return mapping.Key; + // 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 { diff --git a/CryptoExchange.Net/Converters/SystemTextJson/EnumIntWriterConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/EnumIntWriterConverter.cs index 9a0c9a7..a5d9b79 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/EnumIntWriterConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/EnumIntWriterConverter.cs @@ -1,5 +1,4 @@ using System; -using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/CryptoExchange.Net/Converters/SystemTextJson/MessageHandlers/JsonRestMessageHandler.cs b/CryptoExchange.Net/Converters/SystemTextJson/MessageHandlers/JsonRestMessageHandler.cs new file mode 100644 index 0000000..8f36280 --- /dev/null +++ b/CryptoExchange.Net/Converters/SystemTextJson/MessageHandlers/JsonRestMessageHandler.cs @@ -0,0 +1,117 @@ +using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Errors; +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers +{ + /// + /// JSON REST message handler + /// + public abstract class JsonRestMessageHandler : IRestMessageHandler + { + private static MediaTypeWithQualityHeaderValue _acceptJsonContent = new MediaTypeWithQualityHeaderValue(Constants.JsonContentHeader); + + /// + /// Empty rate limit error + /// + protected static readonly ServerRateLimitError _emptyRateLimitError = new ServerRateLimitError(); + + /// + public virtual bool RequiresSeekableStream => false; + + /// + /// The serializer options to use + /// + public abstract JsonSerializerOptions Options { get; } + + /// + public MediaTypeWithQualityHeaderValue AcceptHeader => _acceptJsonContent; + + /// + public virtual ValueTask 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(_emptyRateLimitError); + + var value = retryAfterHeader.Value.First(); + if (int.TryParse(value, out var seconds)) + return new ValueTask(new ServerRateLimitError() { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) }); + + if (DateTime.TryParse(value, out var datetime)) + return new ValueTask(new ServerRateLimitError() { RetryAfter = datetime }); + + return new ValueTask(_emptyRateLimitError); + } + + /// + public abstract ValueTask ParseErrorResponse( + int httpStatusCode, + HttpResponseHeaders responseHeaders, + Stream responseStream); + + /// + public virtual ValueTask CheckForErrorResponse( + RequestDefinition request, + HttpResponseHeaders responseHeaders, + Stream responseStream) => new ValueTask((Error?)null); + + /// + /// Read the response into a JsonDocument object + /// + 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); + } + } + + /// +#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(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(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)); + } + } + + /// + public virtual Error? CheckDeserializedResponse(HttpResponseHeaders responseHeaders, T result) => null; + } +} diff --git a/CryptoExchange.Net/Converters/SystemTextJson/MessageHandlers/JsonSocketMessageHandler.cs b/CryptoExchange.Net/Converters/SystemTextJson/MessageHandlers/JsonSocketMessageHandler.cs new file mode 100644 index 0000000..80a7f77 --- /dev/null +++ b/CryptoExchange.Net/Converters/SystemTextJson/MessageHandlers/JsonSocketMessageHandler.cs @@ -0,0 +1,348 @@ +using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Net.WebSockets; +using System.Text; +using System.Text.Json; + +namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers +{ + /// + /// JSON WebSocket message handler, sequentially read the JSON and looks for specific predefined fields to identify the message + /// + public abstract class JsonSocketMessageHandler : ISocketMessageHandler + { + /// + /// The serializer options to use + /// + public abstract JsonSerializerOptions Options { get; } + + /// + /// Message evaluators + /// + protected abstract MessageTypeDefinition[] TypeEvaluators { get; } + + private readonly SearchResult _searchResult = new(); + + private bool _hasArraySearches; + private bool _initialized; + private int _maxSearchDepth; + private MessageTypeDefinition? _topEvaluator; + private List? _searchFields; + private Dictionary>? _baseTypeMapping; + private Dictionary>? _mapping; + + /// + /// Add a mapping of a specific object of a type to a specific topic + /// + /// Type to get topic for + /// The topic retrieve delegate + protected void AddTopicMapping(Func mapping) + { + _mapping ??= new Dictionary>(); + _mapping.Add(typeof(T), x => mapping((T)x)); + } + + private void InitializeConverter() + { + if (_initialized) + return; + + _maxSearchDepth = int.MinValue; + _searchFields = new List(); + 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? 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; + } + + /// + 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>(); + _baseTypeMapping.Add(deserializedObject.GetType(), mapping); + } + + return mapping(deserializedObject); + } + + mappedBase = true; + currentType = currentType.BaseType; + } + + return null; + } + + /// + public virtual string? GetTypeIdentifier(ReadOnlySpan 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; + } + + /// +#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 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 + } + } +} diff --git a/CryptoExchange.Net/Converters/SystemTextJson/MessageHandlers/JsonSocketPreloadMessageHandler.cs b/CryptoExchange.Net/Converters/SystemTextJson/MessageHandlers/JsonSocketPreloadMessageHandler.cs new file mode 100644 index 0000000..22ae049 --- /dev/null +++ b/CryptoExchange.Net/Converters/SystemTextJson/MessageHandlers/JsonSocketPreloadMessageHandler.cs @@ -0,0 +1,63 @@ +using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters; +using System; +using System.Net.WebSockets; +using System.Text.Json; + +namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers +{ + /// + /// JSON WebSocket message handler, reads the json data info a JsonDocument after which the data can be inspected to identify the message + /// + public abstract class JsonSocketPreloadMessageHandler : ISocketMessageHandler + { + /// + /// The serializer options to use + /// + public abstract JsonSerializerOptions Options { get; } + + /// + public virtual string? GetTypeIdentifier(ReadOnlySpan data, WebSocketMessageType? webSocketMessageType) + { + var reader = new Utf8JsonReader(data); + var jsonDocument = JsonDocument.ParseValue(ref reader); + + return GetTypeIdentifier(jsonDocument); + } + + /// + /// Get the message identifier for this document + /// + protected abstract string? GetTypeIdentifier(JsonDocument document); + + /// + /// Get optional topic filter, for example a symbol name + /// + public virtual string? GetTopicFilter(object deserializedObject) => null; + + /// + public virtual object Deserialize(ReadOnlySpan 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 + } + + /// + /// Get the string value for a path, or an emtpy string if not found + /// + 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; + } + } +} diff --git a/CryptoExchange.Net/Converters/SystemTextJson/NullableEnumConverterFactory.cs b/CryptoExchange.Net/Converters/SystemTextJson/NullableEnumConverterFactory.cs index 4cd7e68..7b63b8d 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/NullableEnumConverterFactory.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/NullableEnumConverterFactory.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Text.Json.Serialization.Metadata; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SerializationModel.cs b/CryptoExchange.Net/Converters/SystemTextJson/SerializationModel.cs index 3c958ec..5a95cdc 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/SerializationModel.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/SerializationModel.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace CryptoExchange.Net.Converters.SystemTextJson { diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SharedQuantityConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/SharedQuantityConverter.cs index bedfebe..6d838f9 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/SharedQuantityConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/SharedQuantityConverter.cs @@ -1,7 +1,5 @@ using CryptoExchange.Net.SharedApis; using System; -using System.Collections.Generic; -using System.Text; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SharedSymbolConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/SharedSymbolConverter.cs index 6622305..ac2238d 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/SharedSymbolConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/SharedSymbolConverter.cs @@ -1,7 +1,5 @@ using CryptoExchange.Net.SharedApis; using System; -using System.Collections.Generic; -using System.Text; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs index c464f23..25e5d8a 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs @@ -2,7 +2,6 @@ using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text; diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageSerializer.cs b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageSerializer.cs index f3cf340..4a628d5 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageSerializer.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageSerializer.cs @@ -1,8 +1,6 @@ using CryptoExchange.Net.Interfaces; using System.Diagnostics.CodeAnalysis; using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; namespace CryptoExchange.Net.Converters.SystemTextJson { diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj index fa81f7d..8a47a44 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.csproj +++ b/CryptoExchange.Net/CryptoExchange.Net.csproj @@ -20,7 +20,7 @@ true https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes enable - 12.0 + latest MIT @@ -45,18 +45,19 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + - - - + + + \ No newline at end of file diff --git a/CryptoExchange.Net/Exceptions/CeDeserializationException.cs b/CryptoExchange.Net/Exceptions/CeDeserializationException.cs new file mode 100644 index 0000000..6b1caf0 --- /dev/null +++ b/CryptoExchange.Net/Exceptions/CeDeserializationException.cs @@ -0,0 +1,24 @@ +using System; + +namespace CryptoExchange.Net.Exceptions +{ + /// + /// Exception during deserialization + /// + public class CeDeserializationException : Exception + { + /// + /// ctor + /// + public CeDeserializationException(string message) : base(message) + { + } + + /// + /// ctor + /// + public CeDeserializationException(string message, Exception innerException) : base(message, innerException) + { + } + } +} diff --git a/CryptoExchange.Net/ExchangeHelpers.cs b/CryptoExchange.Net/ExchangeHelpers.cs index 404f606..6e35421 100644 --- a/CryptoExchange.Net/ExchangeHelpers.cs +++ b/CryptoExchange.Net/ExchangeHelpers.cs @@ -1,7 +1,6 @@ using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.SharedApis; -using CryptoExchange.Net.Sockets; using System; using System.Collections.Generic; using System.Globalization; @@ -376,6 +375,32 @@ namespace CryptoExchange.Net return result; } + /// + /// Queue updates and process them async + /// + /// The queued update type + /// The subscribe call + /// The async update handler + /// 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 fullMode + /// What should happen if the queue contains maxQueuedItems pending updates. If no max is set this setting is ignored + /// Cancellation token to stop the processing + public static async Task ProcessQueuedAsync( + Func, Task> subscribeCall, + Func asyncHandler, + CancellationToken ct, + int? maxQueuedItems = null, + QueueFullBehavior? fullBehavior = null) + { + var processor = new ProcessQueue(asyncHandler, maxQueuedItems, fullBehavior); + await processor.StartAsync().ConfigureAwait(false); + ct.Register(async () => + { + await processor.StopAsync().ConfigureAwait(false); + }); + + await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false); + } + /// /// Queue updates received from a websocket subscriptions and process them async /// @@ -408,7 +433,7 @@ namespace CryptoExchange.Net processor.Exception += result.Data._subscription.InvokeExceptionHandler; result.Data.SubscriptionStatusChanged += (upd) => { - if (upd == CryptoExchange.Net.Objects.SubscriptionStatus.Closed) + if (upd == SubscriptionStatus.Closed) _ = processor.StopAsync(true); }; diff --git a/CryptoExchange.Net/ExchangeSymbolCache.cs b/CryptoExchange.Net/ExchangeSymbolCache.cs index 765b1e1..2b237d4 100644 --- a/CryptoExchange.Net/ExchangeSymbolCache.cs +++ b/CryptoExchange.Net/ExchangeSymbolCache.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Text; namespace CryptoExchange.Net { diff --git a/CryptoExchange.Net/ExtensionMethods.cs b/CryptoExchange.Net/ExtensionMethods.cs index 702b3f5..864d45f 100644 --- a/CryptoExchange.Net/ExtensionMethods.cs +++ b/CryptoExchange.Net/ExtensionMethods.cs @@ -1,18 +1,15 @@ -using System; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.SharedApis; +using Microsoft.Extensions.DependencyInjection; +using System; using System.Collections.Generic; -using System.IO.Compression; +using System.Globalization; using System.IO; +using System.IO.Compression; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Web; -using CryptoExchange.Net.Objects; -using System.Globalization; -using Microsoft.Extensions.DependencyInjection; -using CryptoExchange.Net.SharedApis; -using System.Text.Json.Serialization.Metadata; -using System.Text.Json; -using System.Text.Json.Serialization; namespace CryptoExchange.Net { @@ -64,30 +61,71 @@ namespace CryptoExchange.Net /// public static string CreateParamString(this IDictionary parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType) { - var uriString = string.Empty; - var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList(); - foreach (var arrayEntry in arraysParameters) + var uriString = new StringBuilder(); + bool first = true; + foreach(var parameter in parameters) { - if (serializationType == ArrayParametersSerialization.Array) + if (!first) + uriString.Append("&"); + + first = false; + + if (parameter.GetType().IsArray) { - uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()!) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={string.Format(CultureInfo.InvariantCulture, "{0}", v)}"))}&"; + if (serializationType == ArrayParametersSerialization.Array) + { + foreach(var entry in (object[])parameter.Value) + { + uriString.Append(parameter.Key); + uriString.Append("[]="); + if (urlEncodeValues) + uriString.Append(Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", entry))); + else + uriString.Append(string.Format(CultureInfo.InvariantCulture, "{0}", entry)); + } + } + else if (serializationType == ArrayParametersSerialization.MultipleValues) + { + foreach (var entry in (object[])parameter.Value) + { + uriString.Append(parameter.Key); + uriString.Append("="); + if (urlEncodeValues) + uriString.Append(Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", entry))); + else + uriString.Append(string.Format(CultureInfo.InvariantCulture, "{0}", entry)); + } + } + else + { + uriString.Append('['); + var firstArrayEntry = true; + foreach (var entry in (object[])parameter.Value) + { + if (!firstArrayEntry) + uriString.Append(','); + + firstArrayEntry = false; + if (urlEncodeValues) + uriString.Append(Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", entry))); + else + uriString.Append(string.Format(CultureInfo.InvariantCulture, "{0}", entry)); + } + uriString.Append(']'); + } } - else if (serializationType == ArrayParametersSerialization.MultipleValues) + else { - var array = (Array)arrayEntry.Value; - uriString += string.Join("&", array.OfType().Select(a => $"{arrayEntry.Key}={Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", a))}")); - uriString += "&"; - } - else - { - var array = (Array)arrayEntry.Value; - uriString += $"{arrayEntry.Key}=[{string.Join(",", array.OfType().Select(a => string.Format(CultureInfo.InvariantCulture, "{0}", a)))}]&"; + uriString.Append(parameter.Key); + uriString.Append('='); + if (urlEncodeValues) + uriString.Append(Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", parameter.Value))); + else + uriString.Append(string.Format(CultureInfo.InvariantCulture, "{0}", parameter.Value)); } } - uriString += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={(urlEncodeValues ? Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", s.Value)) : string.Format(CultureInfo.InvariantCulture, "{0}", s.Value))}"))}"; - uriString = uriString.TrimEnd('&'); - return uriString; + return uriString.ToString(); } /// @@ -233,18 +271,16 @@ namespace CryptoExchange.Net /// /// Append a base url with provided path /// - /// - /// - /// public static string AppendPath(this string url, params string[] path) { - if (!url.EndsWith("/")) - url += "/"; + var sb = new StringBuilder(url.TrimEnd('/')); + foreach (var subPath in path) + { + sb.Append('/'); + sb.Append(subPath.Trim('/')); + } - foreach (var item in path) - url += item.Trim('/') + "/"; - - return url.TrimEnd('/'); + return sb.ToString(); } /// @@ -366,19 +402,40 @@ namespace CryptoExchange.Net /// /// Decompress using GzipStream /// - /// - /// + public static ReadOnlySpan DecompressGzip(this ReadOnlySpan data) + { + using var decompressedStream = new MemoryStream(); + using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress); + deflateStream.CopyTo(decompressedStream); + return new ReadOnlySpan(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length); + } + + /// + /// Decompress using GzipStream + /// public static ReadOnlyMemory DecompressGzip(this ReadOnlyMemory data) { using var decompressedStream = new MemoryStream(); using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment) ? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count) : new MemoryStream(data.ToArray()); - using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress); + using var deflateStream = new GZipStream(dataStream, CompressionMode.Decompress); deflateStream.CopyTo(decompressedStream); return new ReadOnlyMemory(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length); } + /// + /// Decompress using GzipStream + /// + public static ReadOnlySpan Decompress(this ReadOnlySpan input) + { + using var output = new MemoryStream(); + using var compressStream = new MemoryStream(input.ToArray()); + using var decompressor = new DeflateStream(compressStream, CompressionMode.Decompress); + decompressor.CopyTo(output); + return new ReadOnlySpan(output.GetBuffer(), 0, (int)output.Length); + } + /// /// Decompress using DeflateStream /// @@ -388,10 +445,9 @@ namespace CryptoExchange.Net { var output = new MemoryStream(); - using (var compressStream = new MemoryStream(input.ToArray())) - using (var decompressor = new DeflateStream(compressStream, CompressionMode.Decompress)) - decompressor.CopyTo(output); - + using var compressStream = new MemoryStream(input.ToArray()); + using var decompressor = new DeflateStream(compressStream, CompressionMode.Decompress); + decompressor.CopyTo(output); output.Position = 0; return new ReadOnlyMemory(output.GetBuffer(), 0, (int)output.Length); } diff --git a/CryptoExchange.Net/Interfaces/IBaseApiClient.cs b/CryptoExchange.Net/Interfaces/Clients/IBaseApiClient.cs similarity index 96% rename from CryptoExchange.Net/Interfaces/IBaseApiClient.cs rename to CryptoExchange.Net/Interfaces/Clients/IBaseApiClient.cs index 21867c8..d7469be 100644 --- a/CryptoExchange.Net/Interfaces/IBaseApiClient.cs +++ b/CryptoExchange.Net/Interfaces/Clients/IBaseApiClient.cs @@ -1,10 +1,9 @@ using CryptoExchange.Net.Authentication; -using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.SharedApis; using System; -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces.Clients { /// /// Base api client diff --git a/CryptoExchange.Net/Interfaces/ICryptoRestClient.cs b/CryptoExchange.Net/Interfaces/Clients/ICryptoRestClient.cs similarity index 88% rename from CryptoExchange.Net/Interfaces/ICryptoRestClient.cs rename to CryptoExchange.Net/Interfaces/Clients/ICryptoRestClient.cs index c3966eb..249c8fc 100644 --- a/CryptoExchange.Net/Interfaces/ICryptoRestClient.cs +++ b/CryptoExchange.Net/Interfaces/Clients/ICryptoRestClient.cs @@ -1,6 +1,6 @@ using System; -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces.Clients { /// /// Client for accessing REST API's for different exchanges diff --git a/CryptoExchange.Net/Interfaces/ICryptoSocketClient.cs b/CryptoExchange.Net/Interfaces/Clients/ICryptoSocketClient.cs similarity index 89% rename from CryptoExchange.Net/Interfaces/ICryptoSocketClient.cs rename to CryptoExchange.Net/Interfaces/Clients/ICryptoSocketClient.cs index 867448c..69cb54d 100644 --- a/CryptoExchange.Net/Interfaces/ICryptoSocketClient.cs +++ b/CryptoExchange.Net/Interfaces/Clients/ICryptoSocketClient.cs @@ -1,6 +1,6 @@ using System; -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces.Clients { /// /// Client for accessing Websocket API's for different exchanges diff --git a/CryptoExchange.Net/Interfaces/IRestApiClient.cs b/CryptoExchange.Net/Interfaces/Clients/IRestApiClient.cs similarity index 89% rename from CryptoExchange.Net/Interfaces/IRestApiClient.cs rename to CryptoExchange.Net/Interfaces/Clients/IRestApiClient.cs index 9d96fbd..09b667d 100644 --- a/CryptoExchange.Net/Interfaces/IRestApiClient.cs +++ b/CryptoExchange.Net/Interfaces/Clients/IRestApiClient.cs @@ -1,4 +1,4 @@ -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces.Clients { /// /// Base rest API client diff --git a/CryptoExchange.Net/Interfaces/IRestClient.cs b/CryptoExchange.Net/Interfaces/Clients/IRestClient.cs similarity index 92% rename from CryptoExchange.Net/Interfaces/IRestClient.cs rename to CryptoExchange.Net/Interfaces/Clients/IRestClient.cs index a2592a3..09d197f 100644 --- a/CryptoExchange.Net/Interfaces/IRestClient.cs +++ b/CryptoExchange.Net/Interfaces/Clients/IRestClient.cs @@ -1,7 +1,7 @@ using System; using CryptoExchange.Net.Objects.Options; -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces.Clients { /// /// Base class for rest API implementations diff --git a/CryptoExchange.Net/Interfaces/ISocketApiClient.cs b/CryptoExchange.Net/Interfaces/Clients/ISocketApiClient.cs similarity index 88% rename from CryptoExchange.Net/Interfaces/ISocketApiClient.cs rename to CryptoExchange.Net/Interfaces/Clients/ISocketApiClient.cs index fdc49bd..47722cc 100644 --- a/CryptoExchange.Net/Interfaces/ISocketApiClient.cs +++ b/CryptoExchange.Net/Interfaces/Clients/ISocketApiClient.cs @@ -1,9 +1,11 @@ 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 +namespace CryptoExchange.Net.Interfaces.Clients { /// /// Socket API client @@ -27,6 +29,10 @@ namespace CryptoExchange.Net.Interfaces /// IWebsocketFactory SocketFactory { get; set; } /// + /// High performance websocket factory + /// + IHighPerfConnectionFactory? HighPerfConnectionFactory { get; set; } + /// /// Current client options /// SocketExchangeOptions ClientOptions { get; } diff --git a/CryptoExchange.Net/Interfaces/ISocketClient.cs b/CryptoExchange.Net/Interfaces/Clients/ISocketClient.cs similarity index 97% rename from CryptoExchange.Net/Interfaces/ISocketClient.cs rename to CryptoExchange.Net/Interfaces/Clients/ISocketClient.cs index 42337ec..ec7a9e3 100644 --- a/CryptoExchange.Net/Interfaces/ISocketClient.cs +++ b/CryptoExchange.Net/Interfaces/Clients/ISocketClient.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Sockets; -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces.Clients { /// /// Base class for socket API implementations diff --git a/CryptoExchange.Net/Interfaces/IMessageAccessor.cs b/CryptoExchange.Net/Interfaces/IMessageAccessor.cs index 1c3c28c..d0a3773 100644 --- a/CryptoExchange.Net/Interfaces/IMessageAccessor.cs +++ b/CryptoExchange.Net/Interfaces/IMessageAccessor.cs @@ -1,7 +1,6 @@ using CryptoExchange.Net.Converters.MessageParsing; using CryptoExchange.Net.Objects; using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading.Tasks; diff --git a/CryptoExchange.Net/Interfaces/IMessageSerializer.cs b/CryptoExchange.Net/Interfaces/IMessageSerializer.cs index 01009a8..be4730c 100644 --- a/CryptoExchange.Net/Interfaces/IMessageSerializer.cs +++ b/CryptoExchange.Net/Interfaces/IMessageSerializer.cs @@ -1,6 +1,4 @@ -using System.Diagnostics.CodeAnalysis; - -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces { /// /// Serializer interface diff --git a/CryptoExchange.Net/Interfaces/IRequest.cs b/CryptoExchange.Net/Interfaces/IRequest.cs index a80f65a..739593a 100644 --- a/CryptoExchange.Net/Interfaces/IRequest.cs +++ b/CryptoExchange.Net/Interfaces/IRequest.cs @@ -1,6 +1,6 @@ using System; -using System.Collections.Generic; using System.Net.Http; +using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; @@ -14,7 +14,7 @@ namespace CryptoExchange.Net.Interfaces /// /// Accept header /// - string Accept { set; } + MediaTypeWithQualityHeaderValue Accept { set; } /// /// Content /// @@ -58,7 +58,7 @@ namespace CryptoExchange.Net.Interfaces /// Get all headers /// /// - KeyValuePair[] GetHeaders(); + HttpRequestHeaders GetHeaders(); /// /// Get the response diff --git a/CryptoExchange.Net/Interfaces/IResponse.cs b/CryptoExchange.Net/Interfaces/IResponse.cs index e8d9346..c4405ca 100644 --- a/CryptoExchange.Net/Interfaces/IResponse.cs +++ b/CryptoExchange.Net/Interfaces/IResponse.cs @@ -1,7 +1,7 @@ using System; -using System.Collections.Generic; using System.IO; using System.Net; +using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; @@ -35,7 +35,7 @@ namespace CryptoExchange.Net.Interfaces /// /// The response headers /// - KeyValuePair[] ResponseHeaders { get; } + HttpResponseHeaders ResponseHeaders { get; } /// /// Get the response stream diff --git a/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs b/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs index 21c2bb7..609373f 100644 --- a/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs +++ b/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using CryptoExchange.Net.Objects; diff --git a/CryptoExchange.Net/Interfaces/IWebsocketFactory.cs b/CryptoExchange.Net/Interfaces/IWebsocketFactory.cs deleted file mode 100644 index 3fe70c1..0000000 --- a/CryptoExchange.Net/Interfaces/IWebsocketFactory.cs +++ /dev/null @@ -1,19 +0,0 @@ -using CryptoExchange.Net.Objects.Sockets; -using Microsoft.Extensions.Logging; - -namespace CryptoExchange.Net.Interfaces -{ - /// - /// Websocket factory interface - /// - public interface IWebsocketFactory - { - /// - /// Create a websocket for an url - /// - /// The logger - /// The parameters to use for the connection - /// - IWebsocket CreateWebsocket(ILogger logger, WebSocketParameters parameters); - } -} diff --git a/CryptoExchange.Net/LibraryHelpers.cs b/CryptoExchange.Net/LibraryHelpers.cs index 5c706a1..1113620 100644 --- a/CryptoExchange.Net/LibraryHelpers.cs +++ b/CryptoExchange.Net/LibraryHelpers.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Net; using System.Net.Http; -using System.Text; namespace CryptoExchange.Net { @@ -69,7 +68,7 @@ namespace CryptoExchange.Net { var reservedLength = brokerId.Length + ClientOrderIdSeparator.Length; - if ((clientOrderId?.Length + reservedLength) > maxLength) + if (clientOrderId?.Length + reservedLength > maxLength) return clientOrderId!; if (!string.IsNullOrEmpty(clientOrderId)) diff --git a/CryptoExchange.Net/Logging/Extensions/RestApiClientLoggingExtensions.cs b/CryptoExchange.Net/Logging/Extensions/RestApiClientLoggingExtensions.cs index f5078d3..84d3d67 100644 --- a/CryptoExchange.Net/Logging/Extensions/RestApiClientLoggingExtensions.cs +++ b/CryptoExchange.Net/Logging/Extensions/RestApiClientLoggingExtensions.cs @@ -22,6 +22,7 @@ namespace CryptoExchange.Net.Logging.Extensions private static readonly Action _restApiCacheHit; private static readonly Action _restApiCacheNotHit; private static readonly Action _restApiCancellationRequested; + private static readonly Action _restApiReceivedResponse; static RestApiClientLoggingExtensions() { @@ -90,6 +91,11 @@ namespace CryptoExchange.Net.Logging.Extensions new EventId(4012, "RestApiCancellationRequested"), "[Req {RequestId}] Request cancelled by user"); + _restApiReceivedResponse = LoggerMessage.Define( + LogLevel.Trace, + new EventId(4013, "RestApiReceivedResponse"), + "[Req {RequestId}] Received response: {Data}"); + } public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error, string? originalData, Exception? exception) @@ -155,5 +161,10 @@ namespace CryptoExchange.Net.Logging.Extensions { _restApiCancellationRequested(logger, requestId, null); } + + public static void RestApiReceivedResponse(this ILogger logger, int requestId, string? originalData) + { + _restApiReceivedResponse(logger, requestId, originalData, null); + } } } diff --git a/CryptoExchange.Net/Logging/Extensions/SocketConnectionLoggingExtension.cs b/CryptoExchange.Net/Logging/Extensions/SocketConnectionLoggingExtension.cs index 67b72eb..3487ce9 100644 --- a/CryptoExchange.Net/Logging/Extensions/SocketConnectionLoggingExtension.cs +++ b/CryptoExchange.Net/Logging/Extensions/SocketConnectionLoggingExtension.cs @@ -1,5 +1,6 @@ using System; using System.Net.WebSockets; +using CryptoExchange.Net.Sockets.Default; using Microsoft.Extensions.Logging; namespace CryptoExchange.Net.Logging.Extensions @@ -8,7 +9,7 @@ namespace CryptoExchange.Net.Logging.Extensions public static class SocketConnectionLoggingExtension { private static readonly Action _activityPaused; - private static readonly Action _socketStatusChanged; + private static readonly Action _socketStatusChanged; private static readonly Action _failedReconnectProcessing; private static readonly Action _unknownExceptionWhileProcessingReconnection; private static readonly Action _webSocketErrorCodeAndDetails; @@ -46,7 +47,7 @@ namespace CryptoExchange.Net.Logging.Extensions new EventId(2000, "ActivityPaused"), "[Sckt {SocketId}] paused activity: {Paused}"); - _socketStatusChanged = LoggerMessage.Define( + _socketStatusChanged = LoggerMessage.Define( LogLevel.Debug, new EventId(2001, "SocketStatusChanged"), "[Sckt {SocketId}] status changed from {OldStatus} to {NewStatus}"); @@ -203,7 +204,7 @@ namespace CryptoExchange.Net.Logging.Extensions _activityPaused(logger, socketId, paused, null); } - public static void SocketStatusChanged(this ILogger logger, int socketId, Sockets.SocketConnection.SocketStatus oldStatus, Sockets.SocketConnection.SocketStatus newStatus) + public static void SocketStatusChanged(this ILogger logger, int socketId, SocketStatus oldStatus, SocketStatus newStatus) { _socketStatusChanged(logger, socketId, oldStatus, newStatus, null); } diff --git a/CryptoExchange.Net/Objects/AssetAlias.cs b/CryptoExchange.Net/Objects/AssetAlias.cs index 5ef6bc7..062c314 100644 --- a/CryptoExchange.Net/Objects/AssetAlias.cs +++ b/CryptoExchange.Net/Objects/AssetAlias.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace CryptoExchange.Net.Objects +namespace CryptoExchange.Net.Objects { /// /// An alias used by the exchange for an asset commonly known by another name diff --git a/CryptoExchange.Net/Objects/AssetAliasConfiguration.cs b/CryptoExchange.Net/Objects/AssetAliasConfiguration.cs index 763b6ec..1bda64f 100644 --- a/CryptoExchange.Net/Objects/AssetAliasConfiguration.cs +++ b/CryptoExchange.Net/Objects/AssetAliasConfiguration.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Text; namespace CryptoExchange.Net.Objects { @@ -23,7 +21,8 @@ namespace CryptoExchange.Net.Objects /// /// Map the common name to an exchange name for an asset. If there is no alias the input name is returned /// - public string CommonToExchangeName(string commonName) => !AutoConvertEnabled ? commonName : Aliases.FirstOrDefault(x => x.CommonAssetName == commonName)?.ExchangeAssetName ?? commonName; + public string CommonToExchangeName(string commonName) => + !AutoConvertEnabled ? commonName : Aliases.FirstOrDefault(x => x.CommonAssetName.Equals(commonName, StringComparison.InvariantCulture))?.ExchangeAssetName ?? commonName; /// /// Map the exchange name to a common name for an asset. If there is no alias the input name is returned @@ -33,7 +32,7 @@ namespace CryptoExchange.Net.Objects if (!AutoConvertEnabled) return exchangeName; - var alias = Aliases.FirstOrDefault(x => x.ExchangeAssetName == exchangeName); + var alias = Aliases.FirstOrDefault(x => x.ExchangeAssetName.Equals(exchangeName, StringComparison.InvariantCulture)); if (alias == null || alias.Type == AliasType.OnlyToExchange) return exchangeName; diff --git a/CryptoExchange.Net/Objects/AsyncAutoResetEvent.cs b/CryptoExchange.Net/Objects/AsyncAutoResetEvent.cs index ff4895d..e34a1a2 100644 --- a/CryptoExchange.Net/Objects/AsyncAutoResetEvent.cs +++ b/CryptoExchange.Net/Objects/AsyncAutoResetEvent.cs @@ -14,6 +14,11 @@ namespace CryptoExchange.Net.Objects { private static readonly Task _completed = Task.FromResult(true); private Queue> _waits = new Queue>(); +#if NET9_0_OR_GREATER + private readonly Lock _waitsLock = new Lock(); +#else + private readonly object _waitsLock = new object(); +#endif private bool _signaled; private readonly bool _reset; @@ -38,7 +43,7 @@ namespace CryptoExchange.Net.Objects try { Task waiter = _completed; - lock (_waits) + lock (_waitsLock) { if (_signaled) { @@ -57,7 +62,7 @@ namespace CryptoExchange.Net.Objects registration = ct.Register(() => { - lock (_waits) + lock (_waitsLock) { tcs.TrySetResult(false); @@ -85,7 +90,7 @@ namespace CryptoExchange.Net.Objects /// public void Set() { - lock (_waits) + lock (_waitsLock) { if (!_reset) { @@ -106,7 +111,9 @@ namespace CryptoExchange.Net.Objects toRelease.TrySetResult(true); } else if (!_signaled) + { _signaled = true; + } } } } diff --git a/CryptoExchange.Net/Objects/CallResult.cs b/CryptoExchange.Net/Objects/CallResult.cs index 7cf297c..74f79ac 100644 --- a/CryptoExchange.Net/Objects/CallResult.cs +++ b/CryptoExchange.Net/Objects/CallResult.cs @@ -1,9 +1,9 @@ using CryptoExchange.Net.SharedApis; using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Text; namespace CryptoExchange.Net.Objects @@ -214,7 +214,7 @@ namespace CryptoExchange.Net.Objects /// /// The headers sent with the request /// - public KeyValuePair[]? RequestHeaders { get; set; } + public HttpRequestHeaders? RequestHeaders { get; set; } /// /// The request id @@ -244,7 +244,7 @@ namespace CryptoExchange.Net.Objects /// /// The response headers /// - public KeyValuePair[]? ResponseHeaders { get; set; } + public HttpResponseHeaders? ResponseHeaders { get; set; } /// /// The time between sending the request and receiving the response @@ -257,14 +257,14 @@ namespace CryptoExchange.Net.Objects public WebCallResult( HttpStatusCode? code, Version? httpVersion, - KeyValuePair[]? responseHeaders, + HttpResponseHeaders? responseHeaders, TimeSpan? responseTime, string? originalData, int? requestId, string? requestUrl, string? requestBody, HttpMethod? requestMethod, - KeyValuePair[]? requestHeaders, + HttpRequestHeaders? requestHeaders, Error? error) : base(error) { ResponseStatusCode = code; @@ -370,7 +370,7 @@ namespace CryptoExchange.Net.Objects /// /// The headers sent with the request /// - public KeyValuePair[]? RequestHeaders { get; set; } + public HttpRequestHeaders? RequestHeaders { get; set; } /// /// The request id @@ -400,7 +400,7 @@ namespace CryptoExchange.Net.Objects /// /// The response headers /// - public KeyValuePair[]? ResponseHeaders { get; set; } + public HttpResponseHeaders? ResponseHeaders { get; set; } /// /// The time between sending the request and receiving the response @@ -418,7 +418,7 @@ namespace CryptoExchange.Net.Objects public WebCallResult( HttpStatusCode? code, Version? httpVersion, - KeyValuePair[]? responseHeaders, + HttpResponseHeaders? responseHeaders, TimeSpan? responseTime, long? responseLength, string? originalData, @@ -426,7 +426,7 @@ namespace CryptoExchange.Net.Objects string? requestUrl, string? requestBody, HttpMethod? requestMethod, - KeyValuePair[]? requestHeaders, + HttpRequestHeaders? requestHeaders, ResultDataSource dataSource, [AllowNull] T data, Error? error) : base(data, originalData, error) diff --git a/CryptoExchange.Net/Objects/Enums.cs b/CryptoExchange.Net/Objects/Enums.cs index 0d4b18c..00a392d 100644 --- a/CryptoExchange.Net/Objects/Enums.cs +++ b/CryptoExchange.Net/Objects/Enums.cs @@ -1,6 +1,4 @@ -using CryptoExchange.Net.Attributes; - -namespace CryptoExchange.Net.Objects +namespace CryptoExchange.Net.Objects { /// /// What to do when a request would exceed the rate limit diff --git a/CryptoExchange.Net/Objects/Error.cs b/CryptoExchange.Net/Objects/Error.cs index 99bcaa5..ea72909 100644 --- a/CryptoExchange.Net/Objects/Error.cs +++ b/CryptoExchange.Net/Objects/Error.cs @@ -79,7 +79,20 @@ namespace CryptoExchange.Net.Objects /// public override string ToString() { - return ErrorCode != null ? $"[{GetType().Name}.{ErrorType}] {ErrorCode}: {Message ?? ErrorDescription}" : $"[{GetType().Name}.{ErrorType}] {Message ?? ErrorDescription}"; + return Code != null + ? $"[{GetType().Name}.{ErrorType}] {Code}: {GetErrorDescription()}" + : $"[{GetType().Name}.{ErrorType}] {GetErrorDescription()}"; + } + + private string GetErrorDescription() + { + if (!string.IsNullOrEmpty(Message)) + return Message!; + + if (ErrorDescription != "Unknown error" || Exception == null) + return ErrorDescription!; + + return Exception.Message; } } diff --git a/CryptoExchange.Net/Objects/Errors/ErrorEvaluator.cs b/CryptoExchange.Net/Objects/Errors/ErrorEvaluator.cs index b245115..364163e 100644 --- a/CryptoExchange.Net/Objects/Errors/ErrorEvaluator.cs +++ b/CryptoExchange.Net/Objects/Errors/ErrorEvaluator.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace CryptoExchange.Net.Objects.Errors { diff --git a/CryptoExchange.Net/Objects/Errors/ErrorInfo.cs b/CryptoExchange.Net/Objects/Errors/ErrorInfo.cs index 0dd38c6..98a5440 100644 --- a/CryptoExchange.Net/Objects/Errors/ErrorInfo.cs +++ b/CryptoExchange.Net/Objects/Errors/ErrorInfo.cs @@ -1,6 +1,4 @@ -using System; - -namespace CryptoExchange.Net.Objects.Errors +namespace CryptoExchange.Net.Objects.Errors { /// /// Error info diff --git a/CryptoExchange.Net/Objects/Errors/ErrorMapping.cs b/CryptoExchange.Net/Objects/Errors/ErrorMapping.cs index 219b3c9..3a144a0 100644 --- a/CryptoExchange.Net/Objects/Errors/ErrorMapping.cs +++ b/CryptoExchange.Net/Objects/Errors/ErrorMapping.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; namespace CryptoExchange.Net.Objects.Errors { diff --git a/CryptoExchange.Net/Objects/Errors/ErrorType.cs b/CryptoExchange.Net/Objects/Errors/ErrorType.cs index 7af5017..4545773 100644 --- a/CryptoExchange.Net/Objects/Errors/ErrorType.cs +++ b/CryptoExchange.Net/Objects/Errors/ErrorType.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace CryptoExchange.Net.Objects.Errors +namespace CryptoExchange.Net.Objects.Errors { /// /// Type of error diff --git a/CryptoExchange.Net/Objects/Options/ApiOptions.cs b/CryptoExchange.Net/Objects/Options/ApiOptions.cs index 855bb08..a46fe79 100644 --- a/CryptoExchange.Net/Objects/Options/ApiOptions.cs +++ b/CryptoExchange.Net/Objects/Options/ApiOptions.cs @@ -8,7 +8,8 @@ namespace CryptoExchange.Net.Objects.Options public class ApiOptions { /// - /// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property + /// If true, the CallResult and DataEvent objects will also include the originally received string data in the OriginalData property. + /// Note that this comes at a performance cost /// public bool? OutputOriginalData { get; set; } diff --git a/CryptoExchange.Net/Objects/Options/ExchangeOptions.cs b/CryptoExchange.Net/Objects/Options/ExchangeOptions.cs index 2d55808..c766a3c 100644 --- a/CryptoExchange.Net/Objects/Options/ExchangeOptions.cs +++ b/CryptoExchange.Net/Objects/Options/ExchangeOptions.cs @@ -14,7 +14,8 @@ namespace CryptoExchange.Net.Objects.Options public ApiProxy? Proxy { get; set; } /// - /// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property + /// If true, the CallResult and DataEvent objects will also include the originally received string data in the OriginalData property. + /// Note that this comes at a performance cost /// public bool OutputOriginalData { get; set; } = false; diff --git a/CryptoExchange.Net/Objects/Options/RestExchangeOptions.cs b/CryptoExchange.Net/Objects/Options/RestExchangeOptions.cs index a000383..86c3884 100644 --- a/CryptoExchange.Net/Objects/Options/RestExchangeOptions.cs +++ b/CryptoExchange.Net/Objects/Options/RestExchangeOptions.cs @@ -1,7 +1,5 @@ using CryptoExchange.Net.Authentication; using System; -using System.Net; -using System.Net.Http; namespace CryptoExchange.Net.Objects.Options { diff --git a/CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs b/CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs index 40e8ea0..a3e7ef4 100644 --- a/CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs +++ b/CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs @@ -32,10 +32,24 @@ namespace CryptoExchange.Net.Objects.Options /// /// The amount of subscriptions that should be made on a single socket connection. Not all API's support multiple subscriptions on a single socket. /// Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a - /// single connection will also increase the amount of traffic on that single connection, potentially leading to issues. + /// single connection will also increase the amount of traffic on that single connection, potentially leading to issues or delays. + /// + /// This setting counts each Subscribe request as one instead of counting the individual subscriptions as does + /// /// public int? SocketSubscriptionsCombineTarget { get; set; } + /// + /// The amount of subscriptions that should be made on a single socket connection. Not all API's support multiple subscriptions on a single socket. + /// Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a + /// single connection will also increase the amount of traffic on that single connection, potentially leading to issues or delays. + /// + /// This setting counts the individual subscriptions in a request instead of counting subscriptions in batched request as one as does. + /// + /// Defaults to 20 + /// + public int SocketIndividualSubscriptionCombineTarget { get; set; } = 20; + /// /// The max amount of connections to make to the server. Can be used for API's which only allow a certain number of connections. Changing this to a high value might cause issues. /// @@ -61,6 +75,11 @@ namespace CryptoExchange.Net.Objects.Options /// public int? ReceiveBufferSize { get; set; } + /// + /// Whether or not to use the updated deserialization logic, default is true + /// + public bool UseUpdatedDeserialization { get; set; } = true; + /// /// Create a copy of this options /// @@ -82,6 +101,7 @@ namespace CryptoExchange.Net.Objects.Options item.RateLimitingBehaviour = RateLimitingBehaviour; item.RateLimiterEnabled = RateLimiterEnabled; item.ReceiveBufferSize = ReceiveBufferSize; + item.UseUpdatedDeserialization = UseUpdatedDeserialization; return item; } } diff --git a/CryptoExchange.Net/Objects/Options/UpdateOptions.cs b/CryptoExchange.Net/Objects/Options/UpdateOptions.cs index 9fc9ed0..3970caa 100644 --- a/CryptoExchange.Net/Objects/Options/UpdateOptions.cs +++ b/CryptoExchange.Net/Objects/Options/UpdateOptions.cs @@ -1,7 +1,5 @@ using CryptoExchange.Net.Authentication; using System; -using System.Collections.Generic; -using System.Text; namespace CryptoExchange.Net.Objects.Options { diff --git a/CryptoExchange.Net/Objects/ParameterCollection.cs b/CryptoExchange.Net/Objects/ParameterCollection.cs index 1512995..daa011d 100644 --- a/CryptoExchange.Net/Objects/ParameterCollection.cs +++ b/CryptoExchange.Net/Objects/ParameterCollection.cs @@ -13,6 +13,15 @@ namespace CryptoExchange.Net.Objects /// public class ParameterCollection : Dictionary { + /// + public new void Add(string key, object value) + { + if (value == null) + throw new ArgumentNullException(key); + + base.Add(key, value); + } + /// /// Add an optional parameter. Not added if value is null /// @@ -21,7 +30,7 @@ namespace CryptoExchange.Net.Objects public void AddOptional(string key, object? value) { if (value != null) - Add(key, value); + base.Add(key, value); } /// @@ -31,7 +40,7 @@ namespace CryptoExchange.Net.Objects /// public void AddString(string key, decimal value) { - Add(key, value.ToString(CultureInfo.InvariantCulture)); + base.Add(key, value.ToString(CultureInfo.InvariantCulture)); } /// @@ -42,7 +51,7 @@ namespace CryptoExchange.Net.Objects public void AddOptionalString(string key, decimal? value) { if (value != null) - Add(key, value.Value.ToString(CultureInfo.InvariantCulture)); + base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture)); } /// @@ -52,7 +61,7 @@ namespace CryptoExchange.Net.Objects /// public void AddString(string key, int value) { - Add(key, value.ToString(CultureInfo.InvariantCulture)); + base.Add(key, value.ToString(CultureInfo.InvariantCulture)); } /// @@ -63,7 +72,7 @@ namespace CryptoExchange.Net.Objects public void AddOptionalString(string key, int? value) { if (value != null) - Add(key, value.Value.ToString(CultureInfo.InvariantCulture)); + base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture)); } /// @@ -73,7 +82,7 @@ namespace CryptoExchange.Net.Objects /// public void AddString(string key, long value) { - Add(key, value.ToString(CultureInfo.InvariantCulture)); + base.Add(key, value.ToString(CultureInfo.InvariantCulture)); } /// @@ -84,7 +93,7 @@ namespace CryptoExchange.Net.Objects public void AddOptionalString(string key, long? value) { if (value != null) - Add(key, value.Value.ToString(CultureInfo.InvariantCulture)); + base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture)); } /// @@ -94,7 +103,7 @@ namespace CryptoExchange.Net.Objects /// public void AddMilliseconds(string key, DateTime value) { - Add(key, DateTimeConverter.ConvertToMilliseconds(value)); + base.Add(key, DateTimeConverter.ConvertToMilliseconds(value)); } /// @@ -105,7 +114,7 @@ namespace CryptoExchange.Net.Objects public void AddOptionalMilliseconds(string key, DateTime? value) { if (value != null) - Add(key, DateTimeConverter.ConvertToMilliseconds(value)); + base.Add(key, DateTimeConverter.ConvertToMilliseconds(value)); } /// @@ -115,7 +124,7 @@ namespace CryptoExchange.Net.Objects /// public void AddMillisecondsString(string key, DateTime value) { - Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture)); + base.Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture)); } /// @@ -126,7 +135,7 @@ namespace CryptoExchange.Net.Objects public void AddOptionalMillisecondsString(string key, DateTime? value) { if (value != null) - Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture)); + base.Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture)); } /// @@ -136,7 +145,7 @@ namespace CryptoExchange.Net.Objects /// public void AddSeconds(string key, DateTime value) { - Add(key, DateTimeConverter.ConvertToSeconds(value)); + base.Add(key, DateTimeConverter.ConvertToSeconds(value)); } /// @@ -147,7 +156,7 @@ namespace CryptoExchange.Net.Objects public void AddOptionalSeconds(string key, DateTime? value) { if (value != null) - Add(key, DateTimeConverter.ConvertToSeconds(value)); + base.Add(key, DateTimeConverter.ConvertToSeconds(value)); } /// @@ -157,7 +166,7 @@ namespace CryptoExchange.Net.Objects /// public void AddSecondsString(string key, DateTime value) { - Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!); + base.Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!); } /// @@ -168,7 +177,7 @@ namespace CryptoExchange.Net.Objects public void AddOptionalSecondsString(string key, DateTime? value) { if (value != null) - Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!); + base.Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!); } /// @@ -181,7 +190,7 @@ namespace CryptoExchange.Net.Objects #endif where T : struct, Enum { - Add(key, EnumConverter.GetString(value)!); + base.Add(key, EnumConverter.GetString(value)!); } /// @@ -197,7 +206,7 @@ namespace CryptoExchange.Net.Objects where T : struct, Enum { var stringVal = EnumConverter.GetString(value)!; - Add(key, int.Parse(stringVal)!); + base.Add(key, int.Parse(stringVal)!); } /// @@ -213,7 +222,7 @@ namespace CryptoExchange.Net.Objects where T : struct, Enum { if (value != null) - Add(key, EnumConverter.GetString(value)); + base.Add(key, EnumConverter.GetString(value)); } /// @@ -229,7 +238,7 @@ namespace CryptoExchange.Net.Objects if (value != null) { var stringVal = EnumConverter.GetString(value); - Add(key, int.Parse(stringVal)); + base.Add(key, int.Parse(stringVal)); } } @@ -243,7 +252,7 @@ namespace CryptoExchange.Net.Objects if (this.Any()) throw new InvalidOperationException("Can't set body when other parameters already specified"); - Add(Constants.BodyPlaceHolderKey, body); + base.Add(Constants.BodyPlaceHolderKey, body); } } } diff --git a/CryptoExchange.Net/Sockets/ProcessQueue.cs b/CryptoExchange.Net/Objects/ProcessQueue.cs similarity index 97% rename from CryptoExchange.Net/Sockets/ProcessQueue.cs rename to CryptoExchange.Net/Objects/ProcessQueue.cs index ab93b72..7fb3121 100644 --- a/CryptoExchange.Net/Sockets/ProcessQueue.cs +++ b/CryptoExchange.Net/Objects/ProcessQueue.cs @@ -1,11 +1,10 @@ -using CryptoExchange.Net.Objects; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; -namespace CryptoExchange.Net.Sockets +namespace CryptoExchange.Net.Objects { /// diff --git a/CryptoExchange.Net/Objects/RestRequestConfiguration.cs b/CryptoExchange.Net/Objects/RestRequestConfiguration.cs index e748b9f..1d98143 100644 --- a/CryptoExchange.Net/Objects/RestRequestConfiguration.cs +++ b/CryptoExchange.Net/Objects/RestRequestConfiguration.cs @@ -30,15 +30,15 @@ namespace CryptoExchange.Net.Objects /// /// Query parameters /// - public IDictionary QueryParameters { get; set; } + public IDictionary? QueryParameters { get; set; } /// /// Body parameters /// - public IDictionary BodyParameters { get; set; } + public IDictionary? BodyParameters { get; set; } /// /// Request headers /// - public IDictionary Headers { get; set; } + public IDictionary? Headers { get; set; } /// /// Array serialization type /// @@ -58,9 +58,9 @@ namespace CryptoExchange.Net.Objects public RestRequestConfiguration( RequestDefinition requestDefinition, string baseAddress, - IDictionary queryParams, - IDictionary bodyParams, - IDictionary headers, + IDictionary? queryParams, + IDictionary? bodyParams, + IDictionary? headers, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parametersPosition, RequestBodyFormat bodyFormat) @@ -83,8 +83,12 @@ namespace CryptoExchange.Net.Objects public IDictionary GetPositionParameters() { if (ParameterPosition == HttpMethodParameterPosition.InBody) + { + BodyParameters ??= new Dictionary(); return BodyParameters; + } + QueryParameters ??= new Dictionary(); return QueryParameters; } @@ -94,7 +98,7 @@ namespace CryptoExchange.Net.Objects /// Whether to URL encode the parameter string if creating new public string GetQueryString(bool urlEncode = true) { - return _queryString ?? QueryParameters.CreateParamString(urlEncode, ArraySerialization); + return _queryString ?? QueryParameters?.CreateParamString(urlEncode, ArraySerialization) ?? string.Empty; } /// diff --git a/CryptoExchange.Net/Objects/Sockets/DataEvent.cs b/CryptoExchange.Net/Objects/Sockets/DataEvent.cs index e808bdf..8780b99 100644 --- a/CryptoExchange.Net/Objects/Sockets/DataEvent.cs +++ b/CryptoExchange.Net/Objects/Sockets/DataEvent.cs @@ -6,8 +6,7 @@ namespace CryptoExchange.Net.Objects.Sockets /// /// An update received from a socket update subscription /// - /// The type of the data - public class DataEvent + public class DataEvent { /// /// The timestamp the data was received @@ -29,6 +28,11 @@ namespace CryptoExchange.Net.Objects.Sockets /// public string? Symbol { get; set; } + /// + /// The exchange name + /// + public string Exchange { get; set; } + /// /// The original data that was received, only available when OutputOriginalData is set to true in the client options /// @@ -39,6 +43,29 @@ namespace CryptoExchange.Net.Objects.Sockets /// public SocketUpdateType? UpdateType { get; set; } + /// + /// ctor + /// + public DataEvent( + string exchange, + DateTime receiveTimestamp, + string? originalData) + { + Exchange = exchange; + OriginalData = originalData; + ReceiveTime = receiveTimestamp; + } + + /// + public override string ToString() + { + return $"{StreamId} - {(Symbol == null ? "" : (Symbol + " - "))}{UpdateType}"; + } + } + + /// + public class DataEvent : DataEvent + { /// /// The received data deserialized into an object /// @@ -47,75 +74,13 @@ namespace CryptoExchange.Net.Objects.Sockets /// /// ctor /// - public DataEvent(T data, string? streamId, string? symbol, string? originalData, DateTime receiveTimestamp, SocketUpdateType? updateType) + public DataEvent( + string exchange, + T data, + DateTime receiveTimestamp, + string? originalData): base(exchange, receiveTimestamp, originalData) { Data = data; - StreamId = streamId; - Symbol = symbol; - OriginalData = originalData; - ReceiveTime = receiveTimestamp; - UpdateType = updateType; - } - - /// - /// Create a new DataEvent with data in the from of type K based on the current DataEvent. Topic, OriginalData and ReceivedTimestamp will be copied over - /// - /// The type of the new data - /// The new data - /// - public DataEvent As(K data) - { - return new DataEvent(data, StreamId, Symbol, OriginalData, ReceiveTime, UpdateType) - { - DataTime = DataTime - }; - } - - /// - /// Create a new DataEvent with data in the from of type K based on the current DataEvent. OriginalData and ReceivedTimestamp will be copied over - /// - /// The type of the new data - /// The new data - /// The new symbol - /// - public DataEvent As(K data, string? symbol) - { - return new DataEvent(data, StreamId, symbol, OriginalData, ReceiveTime, UpdateType) - { - DataTime = DataTime - }; - } - - /// - /// Create a new DataEvent with data in the from of type K based on the current DataEvent. OriginalData and ReceivedTimestamp will be copied over - /// - /// The type of the new data - /// The new data - /// The new stream id - /// The new symbol - /// The type of update - /// - public DataEvent As(K data, string streamId, string? symbol, SocketUpdateType updateType) - { - return new DataEvent(data, streamId, symbol, OriginalData, ReceiveTime, updateType) - { - DataTime = DataTime - }; - } - - /// - /// Copy the WebCallResult to a new data type - /// - /// The new type - /// The exchange the result is for - /// The data - /// - public ExchangeEvent AsExchangeEvent(string exchange, K data) - { - return new ExchangeEvent(exchange, this.As(data)) - { - DataTime = DataTime - }; } /// @@ -123,7 +88,7 @@ namespace CryptoExchange.Net.Objects.Sockets /// /// /// - public DataEvent WithSymbol(string symbol) + public DataEvent WithSymbol(string? symbol) { Symbol = symbol; return this; @@ -161,36 +126,19 @@ namespace CryptoExchange.Net.Objects.Sockets } /// - /// Create a CallResult from this DataEvent + /// Create a new DataEvent of the new type /// - /// - public CallResult ToCallResult() + public DataEvent ToType(TNew data) { - return new CallResult(Data, OriginalData, null); - } - - /// - /// Create a CallResult from this DataEvent - /// - /// - public CallResult ToCallResult(K data) - { - return new CallResult(data, OriginalData, null); - } - - /// - /// Create a CallResult from this DataEvent - /// - /// - public CallResult ToCallResult(Error error) - { - return new CallResult(default, OriginalData, error); + return new DataEvent(Exchange, data, ReceiveTime, OriginalData) + { + StreamId = StreamId, + UpdateType = UpdateType, + Symbol = Symbol + }; } /// - public override string ToString() - { - return $"{StreamId} - {(Symbol == null ? "" : (Symbol + " - "))}{(UpdateType == null ? "" : (UpdateType + " - "))}{Data}"; - } + public override string ToString() => base.ToString().TrimEnd('-') + Data?.ToString(); } } diff --git a/CryptoExchange.Net/Objects/Sockets/HighPerfUpdateSubscription.cs b/CryptoExchange.Net/Objects/Sockets/HighPerfUpdateSubscription.cs new file mode 100644 index 0000000..3522f0c --- /dev/null +++ b/CryptoExchange.Net/Objects/Sockets/HighPerfUpdateSubscription.cs @@ -0,0 +1,101 @@ +using CryptoExchange.Net.Sockets.HighPerf; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace CryptoExchange.Net.Objects.Sockets +{ + /// + /// Subscription to a data stream + /// + public class HighPerfUpdateSubscription + { + private readonly HighPerfSocketConnection _connection; + internal readonly HighPerfSubscription _subscription; + +#if NET9_0_OR_GREATER + private readonly Lock _eventLock = new Lock(); +#else + private readonly object _eventLock = new object(); +#endif + + private bool _connectionEventsSubscribed = true; + private readonly List _connectionClosedEventHandlers = new List(); + + /// + /// Event when the connection is closed and will not be reconnected + /// + public event Action ConnectionClosed + { + add { lock (_eventLock) _connectionClosedEventHandlers.Add(value); } + remove { lock (_eventLock) _connectionClosedEventHandlers.Remove(value); } + } + + /// + /// Event when an exception happens during the handling of the data + /// + public event Action Exception + { + add => _subscription.Exception += value; + remove => _subscription.Exception -= value; + } + + /// + /// The id of the socket + /// + public int SocketId => _connection.SocketId; + + /// + /// The id of the subscription + /// + public int Id => _subscription.Id; + + /// + /// ctor + /// + /// The socket connection the subscription is on + /// The subscription + public HighPerfUpdateSubscription(HighPerfSocketConnection connection, HighPerfSubscription subscription) + { + _connection = connection; + _connection.ConnectionClosed += HandleConnectionClosedEvent; + + _subscription = subscription; + } + + private void UnsubscribeConnectionEvents() + { + lock (_eventLock) + { + if (!_connectionEventsSubscribed) + return; + + _connection.ConnectionClosed -= HandleConnectionClosedEvent; + _connectionEventsSubscribed = false; + } + } + + private void HandleConnectionClosedEvent() + { + UnsubscribeConnectionEvents(); + + List handlers; + lock (_eventLock) + handlers = _connectionClosedEventHandlers.ToList(); + + foreach(var callback in handlers) + callback(); + } + + /// + /// Close the subscription + /// + /// + public Task CloseAsync() + { + return _connection.CloseAsync(); + } + } +} diff --git a/CryptoExchange.Net/Objects/Sockets/UpdateSubscription.cs b/CryptoExchange.Net/Objects/Sockets/UpdateSubscription.cs index a86af2d..f128c79 100644 --- a/CryptoExchange.Net/Objects/Sockets/UpdateSubscription.cs +++ b/CryptoExchange.Net/Objects/Sockets/UpdateSubscription.cs @@ -1,7 +1,8 @@ -using CryptoExchange.Net.Sockets; +using CryptoExchange.Net.Sockets.Default; using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.Objects.Sockets @@ -14,7 +15,12 @@ namespace CryptoExchange.Net.Objects.Sockets private readonly SocketConnection _connection; internal readonly Subscription _subscription; - private object _eventLock = new object(); +#if NET9_0_OR_GREATER + private readonly Lock _eventLock = new Lock(); +#else + private readonly object _eventLock = new object(); +#endif + private bool _connectionEventsSubscribed = true; private List _connectionClosedEventHandlers = new List(); private List _connectionLostEventHandlers = new List(); diff --git a/CryptoExchange.Net/Objects/Sockets/WebSocketParameters.cs b/CryptoExchange.Net/Objects/Sockets/WebSocketParameters.cs index 5ce4c1a..8057716 100644 --- a/CryptoExchange.Net/Objects/Sockets/WebSocketParameters.cs +++ b/CryptoExchange.Net/Objects/Sockets/WebSocketParameters.cs @@ -73,6 +73,11 @@ namespace CryptoExchange.Net.Objects.Sockets /// The buffer size to use for receiving data /// public int? ReceiveBufferSize { get; set; } = null; + + /// + /// Whether or not to use the updated deserialization logic + /// + public bool UseUpdatedDeserialization { get; set; } /// /// ctor diff --git a/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs b/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs index 5a71974..98498f5 100644 --- a/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs +++ b/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs @@ -1,6 +1,5 @@ using CryptoExchange.Net.Interfaces; using System; -using System.Collections.Generic; namespace CryptoExchange.Net.OrderBook { diff --git a/CryptoExchange.Net/OrderBook/ProcessQueueItem.cs b/CryptoExchange.Net/OrderBook/ProcessQueueItem.cs index f23110c..6d16d29 100644 --- a/CryptoExchange.Net/OrderBook/ProcessQueueItem.cs +++ b/CryptoExchange.Net/OrderBook/ProcessQueueItem.cs @@ -1,6 +1,5 @@ using CryptoExchange.Net.Interfaces; using System; -using System.Collections.Generic; namespace CryptoExchange.Net.OrderBook { diff --git a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs index cedb443..9892a54 100644 --- a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs +++ b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs @@ -22,7 +22,11 @@ namespace CryptoExchange.Net.OrderBook /// public abstract class SymbolOrderBook : ISymbolOrderBook, IDisposable { +#if NET9_0_OR_GREATER + private readonly Lock _bookLock = new Lock(); +#else private readonly object _bookLock = new object(); +#endif private OrderBookStatus _status; private UpdateSubscription? _subscription; @@ -473,15 +477,13 @@ namespace CryptoExchange.Net.OrderBook /// protected void CheckProcessBuffer() { - var pbList = _processBuffer.ToList(); - if (pbList.Count > 0) - _logger.OrderBookProcessingBufferedUpdates(Api, Symbol, pbList.Count); + if (_processBuffer.Count > 0) + _logger.OrderBookProcessingBufferedUpdates(Api, Symbol, _processBuffer.Count); - foreach (var bufferEntry in pbList) - { + foreach (var bufferEntry in _processBuffer) ProcessRangeUpdates(bufferEntry.FirstUpdateId, bufferEntry.LastUpdateId, bufferEntry.Bids, bufferEntry.Asks); - _processBuffer.Remove(bufferEntry); - } + + _processBuffer.Clear(); } /// @@ -727,7 +729,9 @@ namespace CryptoExchange.Net.OrderBook LastUpdateId = item.EndUpdateId, }); - _logger.OrderBookUpdateBuffered(Api, Symbol, item.StartUpdateId, item.EndUpdateId, item.Asks.Count(), item.Bids.Count()); + + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.OrderBookUpdateBuffered(Api, Symbol, item.StartUpdateId, item.EndUpdateId, item.Asks.Length, item.Bids.Length); } else { @@ -840,7 +844,8 @@ namespace CryptoExchange.Net.OrderBook LastSequenceNumber = lastUpdateId; - _logger.OrderBookProcessedMessage(Api, Symbol, firstUpdateId, lastUpdateId); + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.OrderBookProcessedMessage(Api, Symbol, firstUpdateId, lastUpdateId); } } diff --git a/CryptoExchange.Net/RateLimiting/Filters/HostFilter.cs b/CryptoExchange.Net/RateLimiting/Filters/HostFilter.cs index 65b47df..4a6dc9f 100644 --- a/CryptoExchange.Net/RateLimiting/Filters/HostFilter.cs +++ b/CryptoExchange.Net/RateLimiting/Filters/HostFilter.cs @@ -21,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters /// public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) - => host == _host; + => host.Equals(_host, System.StringComparison.InvariantCulture); } } diff --git a/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGate.cs b/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGate.cs index eb38e6a..8d9b8dc 100644 --- a/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGate.cs +++ b/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGate.cs @@ -56,7 +56,7 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. /// Cancelation token /// Error if RateLimitingBehaviour is Fail and rate limit is hit - Task ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct); + ValueTask ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct); /// /// Enforces the rate limit as defined in the request definition. When a rate limit is hit will wait for the rate limit to pass if RateLimitingBehaviour is Wait, or return an error if it is set to Fail @@ -73,6 +73,6 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. /// Cancelation token /// Error if RateLimitingBehaviour is Fail and rate limit is hit - Task ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct); + ValueTask ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct); } } diff --git a/CryptoExchange.Net/RateLimiting/RateLimitGate.cs b/CryptoExchange.Net/RateLimiting/RateLimitGate.cs index c07c319..518a105 100644 --- a/CryptoExchange.Net/RateLimiting/RateLimitGate.cs +++ b/CryptoExchange.Net/RateLimiting/RateLimitGate.cs @@ -37,7 +37,7 @@ namespace CryptoExchange.Net.RateLimiting } /// - public async Task ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct) + public async ValueTask ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct) { await _semaphore.WaitAsync(ct).ConfigureAwait(false); bool release = true; @@ -61,7 +61,7 @@ namespace CryptoExchange.Net.RateLimiting } /// - public async Task ProcessSingleAsync( + public async ValueTask ProcessSingleAsync( ILogger logger, int itemId, IRateLimitGuard guard, @@ -95,7 +95,7 @@ namespace CryptoExchange.Net.RateLimiting } } - private async Task CheckGuardsAsync(IEnumerable guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct) + private async ValueTask CheckGuardsAsync(IEnumerable guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct) { foreach (var guard in guards) { @@ -139,10 +139,13 @@ namespace CryptoExchange.Net.RateLimiting { RateLimitUpdated?.Invoke(new RateLimitUpdateEvent(itemId, _name, guard.Description, result.Current, result.Limit, result.Period)); - if (type == RateLimitItemType.Connection) - logger.RateLimitAppliedConnection(itemId, guard.Name, guard.Description, result.Current); - else - logger.RateLimitAppliedRequest(itemId, definition.Path, guard.Name, guard.Description, result.Current); + if (logger.IsEnabled(LogLevel.Trace)) + { + if (type == RateLimitItemType.Connection) + logger.RateLimitAppliedConnection(itemId, guard.Name, guard.Description, result.Current); + else + logger.RateLimitAppliedRequest(itemId, definition.Path, guard.Name, guard.Description, result.Current); + } } } diff --git a/CryptoExchange.Net/RateLimiting/RateLimitUpdateEvent.cs b/CryptoExchange.Net/RateLimiting/RateLimitUpdateEvent.cs index 341b822..83ddc6a 100644 --- a/CryptoExchange.Net/RateLimiting/RateLimitUpdateEvent.cs +++ b/CryptoExchange.Net/RateLimiting/RateLimitUpdateEvent.cs @@ -1,5 +1,4 @@ -using CryptoExchange.Net.Objects; -using System; +using System; namespace CryptoExchange.Net.RateLimiting { diff --git a/CryptoExchange.Net/Requests/Request.cs b/CryptoExchange.Net/Requests/Request.cs index f563253..698db12 100644 --- a/CryptoExchange.Net/Requests/Request.cs +++ b/CryptoExchange.Net/Requests/Request.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; @@ -35,9 +33,9 @@ namespace CryptoExchange.Net.Requests public string? Content { get; private set; } /// - public string Accept + public MediaTypeWithQualityHeaderValue Accept { - set => _request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(value)); + set => _request.Headers.Accept.Add(value); } /// @@ -70,9 +68,9 @@ namespace CryptoExchange.Net.Requests } /// - public KeyValuePair[] GetHeaders() + public HttpRequestHeaders GetHeaders() { - return _request.Headers.Select(h => new KeyValuePair(h.Key, h.Value.ToArray())).ToArray(); + return _request.Headers; } /// diff --git a/CryptoExchange.Net/Requests/RequestFactory.cs b/CryptoExchange.Net/Requests/RequestFactory.cs index 7523d18..b70064e 100644 --- a/CryptoExchange.Net/Requests/RequestFactory.cs +++ b/CryptoExchange.Net/Requests/RequestFactory.cs @@ -1,5 +1,4 @@ using System; -using System.Net; using System.Net.Http; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; diff --git a/CryptoExchange.Net/Requests/Response.cs b/CryptoExchange.Net/Requests/Response.cs index 61f53ee..7222e2b 100644 --- a/CryptoExchange.Net/Requests/Response.cs +++ b/CryptoExchange.Net/Requests/Response.cs @@ -1,10 +1,10 @@ +using System; using CryptoExchange.Net.Interfaces; -using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; @@ -30,7 +30,7 @@ namespace CryptoExchange.Net.Requests public long? ContentLength => _response.Content.Headers.ContentLength; /// - public KeyValuePair[] ResponseHeaders => _response.Headers.Select(x => new KeyValuePair(x.Key, x.Value.ToArray())).ToArray(); + public HttpResponseHeaders ResponseHeaders => _response.Headers; /// /// Create response for a http response message diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedAccountType.cs b/CryptoExchange.Net/SharedApis/Enums/SharedAccountType.cs index e3bb862..c7fb54b 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedAccountType.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedAccountType.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Account type diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedTickerType.cs b/CryptoExchange.Net/SharedApis/Enums/SharedTickerType.cs index ccf6355..1525219 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedTickerType.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedTickerType.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Type of ticker diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedTpSlSide.cs b/CryptoExchange.Net/SharedApis/Enums/SharedTpSlSide.cs index c8cd0ab..d5bae45 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedTpSlSide.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedTpSlSide.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Take Profit / Stop Loss side diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedTriggerOrderDirection.cs b/CryptoExchange.Net/SharedApis/Enums/SharedTriggerOrderDirection.cs index 1aefff9..8bd7791 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedTriggerOrderDirection.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedTriggerOrderDirection.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// The order direction when order trigger parameters are reached diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedTriggerOrderStatus.cs b/CryptoExchange.Net/SharedApis/Enums/SharedTriggerOrderStatus.cs index 60082a9..e38e051 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedTriggerOrderStatus.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedTriggerOrderStatus.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Trigger order status diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedTriggerPriceDirection.cs b/CryptoExchange.Net/SharedApis/Enums/SharedTriggerPriceDirection.cs index be513d2..12e78f1 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedTriggerPriceDirection.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedTriggerPriceDirection.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Price direction for trigger order diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedTriggerPriceType.cs b/CryptoExchange.Net/SharedApis/Enums/SharedTriggerPriceType.cs index 84a2a1c..1a12792 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedTriggerPriceType.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedTriggerPriceType.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Price direction for trigger order diff --git a/CryptoExchange.Net/SharedApis/Interfaces/ISharedClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/ISharedClient.cs index eca53e9..da7a1a4 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/ISharedClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/ISharedClient.cs @@ -1,5 +1,4 @@ -using CryptoExchange.Net.Objects; -using System; +using System; namespace CryptoExchange.Net.SharedApis { diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFundingRateRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFundingRateRestClient.cs index 0553db7..057cb36 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFundingRateRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFundingRateRestClient.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderClientIdRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderClientIdRestClient.cs index f937403..2f8e925 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderClientIdRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderClientIdRestClient.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; +using System.Threading.Tasks; using System.Threading; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderRestClient.cs index 42b2ef1..5c3af62 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderRestClient.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesSymbolRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesSymbolRestClient.cs index 672ef51..81b27db 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesSymbolRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesSymbolRestClient.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTickerRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTickerRestClient.cs index f96b9b3..8343139 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTickerRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTickerRestClient.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTpSlRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTpSlRestClient.cs index adc9f94..9f62e8b 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTpSlRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTpSlRestClient.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; +using System.Threading.Tasks; using System.Threading; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTriggerOrderRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTriggerOrderRestClient.cs index 176f2ef..bdda1c8 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTriggerOrderRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTriggerOrderRestClient.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IIndexPriceKlineRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IIndexPriceKlineRestClient.cs index 103055d..4c2299f 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IIndexPriceKlineRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IIndexPriceKlineRestClient.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IMarkPriceKlineRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IMarkPriceKlineRestClient.cs index 69f2f5c..0b02528 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IMarkPriceKlineRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IMarkPriceKlineRestClient.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionHistoryRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionHistoryRestClient.cs index 8097fff..5905927 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionHistoryRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionHistoryRestClient.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IAssetsRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IAssetsRestClient.cs index a688386..bea8a31 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IAssetsRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IAssetsRestClient.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBalanceRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBalanceRestClient.cs index 8190fc4..adf3e6d 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBalanceRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBalanceRestClient.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IDepositRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IDepositRestClient.cs index 0229d4a..c50758e 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IDepositRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IDepositRestClient.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IFeeRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IFeeRestClient.cs index 3a012cb..9f73ff0 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IFeeRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IFeeRestClient.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; +using System.Threading.Tasks; using System.Threading; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IKlineRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IKlineRestClient.cs index 06cee87..a71ac1e 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IKlineRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IKlineRestClient.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IRecentTradeRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IRecentTradeRestClient.cs index 714b7d5..ea1aad8 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IRecentTradeRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IRecentTradeRestClient.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITradeHistoryRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITradeHistoryRestClient.cs index e14c64d..c20d9ce 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITradeHistoryRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITradeHistoryRestClient.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITransferRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITransferRestClient.cs index 23eabab..32e0f07 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITransferRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITransferRestClient.cs @@ -1,6 +1,4 @@ -using CryptoExchange.Net.Objects; -using System.Collections.Generic; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawalRestClient .cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawalRestClient .cs index 3316888..c2f3600 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawalRestClient .cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawalRestClient .cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderClientIdRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderClientIdRestClient.cs index c77a76c..438eb44 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderClientIdRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderClientIdRestClient.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; +using System.Threading.Tasks; using System.Threading; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderRestClient.cs index cb5e816..c4d1a32 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderRestClient.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotSymbolRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotSymbolRestClient.cs index 520f07c..78cb0f4 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotSymbolRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotSymbolRestClient.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTickerRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTickerRestClient.cs index 5e9a8f3..68f18d6 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTickerRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTickerRestClient.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTriggerOrderRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTriggerOrderRestClient.cs index a3009c7..871019c 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTriggerOrderRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTriggerOrderRestClient.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IFuturesOrderSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IFuturesOrderSocketClient.cs index db22ab6..26d1529 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IFuturesOrderSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IFuturesOrderSocketClient.cs @@ -1,6 +1,5 @@ using CryptoExchange.Net.Objects.Sockets; using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -23,6 +22,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToFuturesOrderUpdatesAsync(SubscribeFuturesOrderRequest request, Action> handler, CancellationToken ct = default); + Task> SubscribeToFuturesOrderUpdatesAsync(SubscribeFuturesOrderRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IPositionSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IPositionSocketClient.cs index 03cba56..05697e3 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IPositionSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IPositionSocketClient.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; using System.Threading; using CryptoExchange.Net.Objects.Sockets; @@ -23,6 +22,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToPositionUpdatesAsync(SubscribePositionRequest request, Action> handler, CancellationToken ct = default); + Task> SubscribeToPositionUpdatesAsync(SubscribePositionRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBalanceSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBalanceSocketClient.cs index 24c66a6..5591079 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBalanceSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBalanceSocketClient.cs @@ -1,6 +1,5 @@ using CryptoExchange.Net.Objects.Sockets; using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -23,6 +22,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToBalanceUpdatesAsync(SubscribeBalancesRequest request, Action> handler, CancellationToken ct = default); + Task> SubscribeToBalanceUpdatesAsync(SubscribeBalancesRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBookTickerSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBookTickerSocketClient.cs index ad2c575..46fcf29 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBookTickerSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBookTickerSocketClient.cs @@ -22,6 +22,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToBookTickerUpdatesAsync(SubscribeBookTickerRequest request, Action> handler, CancellationToken ct = default); + Task> SubscribeToBookTickerUpdatesAsync(SubscribeBookTickerRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IKlineSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IKlineSocketClient.cs index 9a8c520..1133940 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IKlineSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IKlineSocketClient.cs @@ -22,6 +22,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToKlineUpdatesAsync(SubscribeKlineRequest request, Action> handler, CancellationToken ct = default); + Task> SubscribeToKlineUpdatesAsync(SubscribeKlineRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IOrderBookSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IOrderBookSocketClient.cs index 4cab64f..9e039f2 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IOrderBookSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IOrderBookSocketClient.cs @@ -22,6 +22,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToOrderBookUpdatesAsync(SubscribeOrderBookRequest request, Action> handler, CancellationToken ct = default); + Task> SubscribeToOrderBookUpdatesAsync(SubscribeOrderBookRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickerSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickerSocketClient.cs index 89ce996..c441972 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickerSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickerSocketClient.cs @@ -22,6 +22,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToTickerUpdatesAsync(SubscribeTickerRequest request, Action> handler, CancellationToken ct = default); + Task> SubscribeToTickerUpdatesAsync(SubscribeTickerRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickersSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickersSocketClient.cs index 4d25ffb..9221b09 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickersSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickersSocketClient.cs @@ -1,6 +1,5 @@ using CryptoExchange.Net.Objects.Sockets; using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -23,6 +22,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToAllTickersUpdatesAsync(SubscribeAllTickersRequest request, Action> handler, CancellationToken ct = default); + Task> SubscribeToAllTickersUpdatesAsync(SubscribeAllTickersRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITradeSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITradeSocketClient.cs index e6a8f21..2572fc6 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITradeSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITradeSocketClient.cs @@ -1,6 +1,5 @@ using CryptoExchange.Net.Objects.Sockets; using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -23,6 +22,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToTradeUpdatesAsync(SubscribeTradeRequest request, Action> handler, CancellationToken ct = default); + Task> SubscribeToTradeUpdatesAsync(SubscribeTradeRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IUserTradeSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IUserTradeSocketClient.cs index 031e9f8..50ccefa 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IUserTradeSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IUserTradeSocketClient.cs @@ -1,6 +1,5 @@ using CryptoExchange.Net.Objects.Sockets; using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -23,6 +22,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToUserTradeUpdatesAsync(SubscribeUserTradeRequest request, Action> handler, CancellationToken ct = default); + Task> SubscribeToUserTradeUpdatesAsync(SubscribeUserTradeRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/Spot/ISpotOrderSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/Spot/ISpotOrderSocketClient.cs index 12d24b2..b07f784 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/Spot/ISpotOrderSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/Spot/ISpotOrderSocketClient.cs @@ -1,6 +1,5 @@ using CryptoExchange.Net.Objects.Sockets; using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -23,6 +22,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToSpotOrderUpdatesAsync(SubscribeSpotOrderRequest request, Action> handler, CancellationToken ct = default); + Task> SubscribeToSpotOrderUpdatesAsync(SubscribeSpotOrderRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Models/ExchangeEvent.cs b/CryptoExchange.Net/SharedApis/Models/ExchangeEvent.cs deleted file mode 100644 index 6e1df01..0000000 --- a/CryptoExchange.Net/SharedApis/Models/ExchangeEvent.cs +++ /dev/null @@ -1,34 +0,0 @@ -using CryptoExchange.Net.Objects.Sockets; - -namespace CryptoExchange.Net.SharedApis -{ - /// - /// An update event for a specific exchange - /// - /// Type of the data - public class ExchangeEvent : DataEvent - { - /// - /// The exchange - /// - public string Exchange { get; } - - /// - /// ctor - /// - public ExchangeEvent(string exchange, DataEvent evnt) : - base(evnt.Data, - evnt.StreamId, - evnt.Symbol, - evnt.OriginalData, - evnt.ReceiveTime, - evnt.UpdateType) - { - DataTime = evnt.DataTime; - Exchange = exchange; - } - - /// - public override string ToString() => $"{Exchange} - " + base.ToString(); - } -} diff --git a/CryptoExchange.Net/SharedApis/Models/ExchangeParameters.cs b/CryptoExchange.Net/SharedApis/Models/ExchangeParameters.cs index 85f838b..8023bca 100644 --- a/CryptoExchange.Net/SharedApis/Models/ExchangeParameters.cs +++ b/CryptoExchange.Net/SharedApis/Models/ExchangeParameters.cs @@ -39,8 +39,8 @@ namespace CryptoExchange.Net.SharedApis /// public bool HasValue(string exchange, string name, Type type) { - var val = _parameters.SingleOrDefault(x => x.Exchange == exchange && x.Name == name); - val ??= _staticParameters.SingleOrDefault(x => x.Exchange == exchange && x.Name == name); + var val = _parameters.SingleOrDefault(x => x.Exchange.Equals(exchange, StringComparison.InvariantCulture) && x.Name.Equals(name, StringComparison.InvariantCulture)); + val ??= _staticParameters.SingleOrDefault(x => x.Exchange.Equals(exchange, StringComparison.InvariantCulture) && x.Name.Equals(name, StringComparison.InvariantCulture)); if (val == null) return false; @@ -71,7 +71,7 @@ namespace CryptoExchange.Net.SharedApis if (provided == true) return true; - var val = _staticParameters.SingleOrDefault(x => x.Exchange == exchange && x.Name == name); + var val = _staticParameters.SingleOrDefault(x => x.Exchange.Equals(exchange, StringComparison.InvariantCulture) && x.Name.Equals(name, StringComparison.InvariantCulture)); if (val == null) return false; @@ -95,7 +95,7 @@ namespace CryptoExchange.Net.SharedApis /// Parameter name public T? GetValue(string exchange, string name) { - var val = _parameters.SingleOrDefault(x => x.Exchange == exchange && x.Name == name); + var val = _parameters.SingleOrDefault(x => x.Exchange.Equals(exchange, StringComparison.InvariantCulture) && x.Name.Equals(name, StringComparison.InvariantCulture)); if (val == null) return default; @@ -122,7 +122,7 @@ namespace CryptoExchange.Net.SharedApis T? value; if (exchangeParameters == null) { - var parameter = _staticParameters.SingleOrDefault(x => x.Exchange == exchange && x.Name == name); + var parameter = _staticParameters.SingleOrDefault(x => x.Exchange.Equals(exchange, StringComparison.InvariantCulture) && x.Name.Equals(name, StringComparison.InvariantCulture)); if (parameter == null) return default; @@ -155,7 +155,7 @@ namespace CryptoExchange.Net.SharedApis /// Parameter value public static void SetStaticParameter(string exchange, string key, object value) { - var existing = _staticParameters.SingleOrDefault(x => x.Exchange == exchange && x.Name == key); + var existing = _staticParameters.SingleOrDefault(x => x.Exchange.Equals(exchange, StringComparison.InvariantCulture) && x.Name.Equals(key, StringComparison.InvariantCulture)); if (existing != null) { existing.Value = value; diff --git a/CryptoExchange.Net/SharedApis/Models/ExchangeWebResult.cs b/CryptoExchange.Net/SharedApis/Models/ExchangeWebResult.cs index f3f7ea0..7861d21 100644 --- a/CryptoExchange.Net/SharedApis/Models/ExchangeWebResult.cs +++ b/CryptoExchange.Net/SharedApis/Models/ExchangeWebResult.cs @@ -1,9 +1,9 @@ using CryptoExchange.Net.Objects; using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Net.Http; using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; namespace CryptoExchange.Net.SharedApis { @@ -103,7 +103,7 @@ namespace CryptoExchange.Net.SharedApis TradingMode[]? dataTradeModes, HttpStatusCode? code, Version? httpVersion, - KeyValuePair[]? responseHeaders, + HttpResponseHeaders? responseHeaders, TimeSpan? responseTime, long? responseLength, string? originalData, @@ -111,7 +111,7 @@ namespace CryptoExchange.Net.SharedApis string? requestUrl, string? requestBody, HttpMethod? requestMethod, - KeyValuePair[]? requestHeaders, + HttpRequestHeaders? requestHeaders, ResultDataSource dataSource, [AllowNull] T data, Error? error, diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/EndpointOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/EndpointOptions.cs index 8d4d552..58b8d1d 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/EndpointOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/EndpointOptions.cs @@ -1,7 +1,6 @@ using CryptoExchange.Net.Objects; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetKlinesOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetKlinesOptions.cs index a5e496f..c171259 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetKlinesOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetKlinesOptions.cs @@ -1,6 +1,5 @@ using CryptoExchange.Net.Objects; using System; -using System.Collections.Generic; using System.Linq; using System.Text; diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOrderBookOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOrderBookOptions.cs index a7ff8e9..074c6a0 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOrderBookOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOrderBookOptions.cs @@ -1,6 +1,5 @@ using CryptoExchange.Net.Objects; using System; -using System.Collections.Generic; using System.Linq; using System.Text; diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTickerOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTickerOptions.cs index 564cd40..fee07ca 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTickerOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTickerOptions.cs @@ -1,8 +1,4 @@ -using CryptoExchange.Net.Objects; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Text; namespace CryptoExchange.Net.SharedApis { diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTickersOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTickersOptions.cs index 1d988e6..84885a7 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTickersOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTickersOptions.cs @@ -1,8 +1,4 @@ -using CryptoExchange.Net.Objects; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Text; namespace CryptoExchange.Net.SharedApis { diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PaginatedEndpointOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PaginatedEndpointOptions.cs index 10e98bb..ed0e496 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PaginatedEndpointOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PaginatedEndpointOptions.cs @@ -1,5 +1,4 @@ -using CryptoExchange.Net.Objects; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using System.Text; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesOrderOptions.cs index 8a7c9b1..4683cce 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesOrderOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesOrderOptions.cs @@ -1,6 +1,5 @@ using CryptoExchange.Net.Objects; using System; -using System.Collections.Generic; using System.Linq; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesTriggerOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesTriggerOrderOptions.cs index a6e43db..e68f671 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesTriggerOrderOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesTriggerOrderOptions.cs @@ -1,7 +1,4 @@ using CryptoExchange.Net.Objects; -using System; -using System.Collections.Generic; -using System.Linq; namespace CryptoExchange.Net.SharedApis { diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotOrderOptions.cs index 7e3c2e3..de6a6f2 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotOrderOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotOrderOptions.cs @@ -1,6 +1,5 @@ using CryptoExchange.Net.Objects; using System; -using System.Collections.Generic; using System.Linq; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotTriggerOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotTriggerOrderOptions.cs index ce7fd30..b9323a0 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotTriggerOrderOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotTriggerOrderOptions.cs @@ -1,7 +1,4 @@ using CryptoExchange.Net.Objects; -using System; -using System.Collections.Generic; -using System.Linq; namespace CryptoExchange.Net.SharedApis { diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeKlineOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeKlineOptions.cs index 826c964..ac6f58b 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeKlineOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeKlineOptions.cs @@ -1,6 +1,5 @@ using CryptoExchange.Net.Objects; using System; -using System.Collections.Generic; using System.Linq; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeOrderBookOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeOrderBookOptions.cs index c2baed2..8c1abe7 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeOrderBookOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeOrderBookOptions.cs @@ -1,6 +1,5 @@ using CryptoExchange.Net.Objects; using System; -using System.Collections.Generic; using System.Linq; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeTickerOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeTickerOptions.cs index a2ccfa8..de629fb 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeTickerOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeTickerOptions.cs @@ -1,9 +1,4 @@ -using CryptoExchange.Net.Objects; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Options for subscribing to ticker updates diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeTickersOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeTickersOptions.cs index e3f70d0..3852d9f 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeTickersOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeTickersOptions.cs @@ -1,9 +1,4 @@ -using CryptoExchange.Net.Objects; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Options for subscribing to ticker updates diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetAssetsRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetAssetsRequest.cs index 7ef35a0..e46f3ec 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetAssetsRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetAssetsRequest.cs @@ -1,6 +1,4 @@ -using CryptoExchange.Net.Objects; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Request to retrieve a list of supported assets diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetBalancesRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetBalancesRequest.cs index 50a4852..78595a9 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetBalancesRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetBalancesRequest.cs @@ -1,6 +1,4 @@ -using CryptoExchange.Net.Objects; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Request to retrieve balance info for the user diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetFeeRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetFeeRequest.cs index 429e258..adce155 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetFeeRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetFeeRequest.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Request to retrieve trading fees diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetOpenOrdersRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetOpenOrdersRequest.cs index 9f35a10..f4062ee 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetOpenOrdersRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetOpenOrdersRequest.cs @@ -1,6 +1,4 @@ -using CryptoExchange.Net.Objects; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Request to retrieve the current open orders diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionHistoryRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionHistoryRequest.cs index cc2ec6e..7bbb209 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionHistoryRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionHistoryRequest.cs @@ -1,5 +1,4 @@ -using CryptoExchange.Net.Objects; -using System; +using System; namespace CryptoExchange.Net.SharedApis { diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionModeRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionModeRequest.cs index 7ff9b11..6aaae50 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionModeRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionModeRequest.cs @@ -1,6 +1,4 @@ -using CryptoExchange.Net.Objects; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Request to retrieve the current position mode diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionsRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionsRequest.cs index bed5794..3f6e7c5 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionsRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionsRequest.cs @@ -1,6 +1,4 @@ -using CryptoExchange.Net.Objects; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Request to retrieve open positions diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetSymbolsRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetSymbolsRequest.cs index d90a10e..59fa978 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetSymbolsRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetSymbolsRequest.cs @@ -1,6 +1,4 @@ -using CryptoExchange.Net.Objects; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Request to retrieve symbol info diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetTickersRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetTickersRequest.cs index c3267f4..389e4e1 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetTickersRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetTickersRequest.cs @@ -1,7 +1,4 @@ - -using CryptoExchange.Net.Objects; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Request to retrieve all symbol tickers diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/KeepAliveListenKeyRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/KeepAliveListenKeyRequest.cs index 410f257..459a97f 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/KeepAliveListenKeyRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/KeepAliveListenKeyRequest.cs @@ -1,6 +1,4 @@ -using CryptoExchange.Net.Objects; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Request to keep-alive the update stream for the specified listen key diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/SetPositionModeRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/SetPositionModeRequest.cs index 0d65f0c..75b613c 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/SetPositionModeRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/SetPositionModeRequest.cs @@ -1,6 +1,4 @@ -using CryptoExchange.Net.Objects; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Request to change the current position mode diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/StartListenKeyRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/StartListenKeyRequest.cs index b6dcb45..1f6c81b 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/StartListenKeyRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/StartListenKeyRequest.cs @@ -1,6 +1,4 @@ -using CryptoExchange.Net.Objects; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Request to start the update stream for the current user diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/StopListenKeyRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/StopListenKeyRequest.cs index 4a59a8a..015cc13 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/StopListenKeyRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/StopListenKeyRequest.cs @@ -1,6 +1,4 @@ -using CryptoExchange.Net.Objects; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Request to stop the update stream for the specific listen key diff --git a/CryptoExchange.Net/SharedApis/Models/SharedSymbolRequest.cs b/CryptoExchange.Net/SharedApis/Models/SharedSymbolRequest.cs index a50355c..c277887 100644 --- a/CryptoExchange.Net/SharedApis/Models/SharedSymbolRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/SharedSymbolRequest.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeAllTickersRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeAllTickersRequest.cs index c2e38cb..8eca717 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeAllTickersRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeAllTickersRequest.cs @@ -1,6 +1,4 @@ -using CryptoExchange.Net.Objects; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Request to subscribe to ticker updates for all symbols diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeBalancesRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeBalancesRequest.cs index 0179434..d9945fb 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeBalancesRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeBalancesRequest.cs @@ -1,6 +1,4 @@ -using CryptoExchange.Net.Objects; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Request to subscribe to balance updates diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeFuturesOrderRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeFuturesOrderRequest.cs index a27ba51..716b9c9 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeFuturesOrderRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeFuturesOrderRequest.cs @@ -1,6 +1,4 @@ -using CryptoExchange.Net.Objects; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Request to subscribe to futures order updates diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribePositionRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribePositionRequest.cs index 6ead352..b7c74a5 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribePositionRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribePositionRequest.cs @@ -1,6 +1,4 @@ -using CryptoExchange.Net.Objects; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Request to subscribe to position updates diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeTickerRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeTickerRequest.cs index 46f3636..406bd6f 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeTickerRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeTickerRequest.cs @@ -1,5 +1,4 @@ -using System.Collections; -using System.Collections.Generic; +using System.Collections.Generic; namespace CryptoExchange.Net.SharedApis { diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeUserTradeRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeUserTradeRequest.cs index d4df63c..a851f34 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeUserTradeRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeUserTradeRequest.cs @@ -1,6 +1,4 @@ -using CryptoExchange.Net.Objects; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Request to subscribe to user trade updates diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedAsset.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedAsset.cs index 7151653..00dd301 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedAsset.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedAsset.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; namespace CryptoExchange.Net.SharedApis { diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedFee.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFee.cs index 528ed6d..7f2e24d 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedFee.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFee.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Trading fee info diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesTriggerOrder.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesTriggerOrder.cs index 49fdbf8..734bf71 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesTriggerOrder.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesTriggerOrder.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace CryptoExchange.Net.SharedApis { diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedOrderBook.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedOrderBook.cs index d4e85c9..7057c41 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedOrderBook.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedOrderBook.cs @@ -1,5 +1,4 @@ using CryptoExchange.Net.Interfaces; -using System.Collections.Generic; namespace CryptoExchange.Net.SharedApis { diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotTriggerOrder.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotTriggerOrder.cs index fedade0..ee2a13f 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotTriggerOrder.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotTriggerOrder.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace CryptoExchange.Net.SharedApis { diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedSymbolModel.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedSymbolModel.cs index d15849f..70cf848 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedSymbolModel.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedSymbolModel.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis { /// /// Symbol model diff --git a/CryptoExchange.Net/SharedApis/SharedQuantity.cs b/CryptoExchange.Net/SharedApis/SharedQuantity.cs index 722c1b7..a6c83a4 100644 --- a/CryptoExchange.Net/SharedApis/SharedQuantity.cs +++ b/CryptoExchange.Net/SharedApis/SharedQuantity.cs @@ -1,7 +1,4 @@ using CryptoExchange.Net.Converters.SystemTextJson; -using System; -using System.Collections.Generic; -using System.Text; using System.Text.Json.Serialization; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/SharedApis/SharedSymbol.cs b/CryptoExchange.Net/SharedApis/SharedSymbol.cs index f71ccb8..64d26d7 100644 --- a/CryptoExchange.Net/SharedApis/SharedSymbol.cs +++ b/CryptoExchange.Net/SharedApis/SharedSymbol.cs @@ -1,7 +1,5 @@ using CryptoExchange.Net.Converters.SystemTextJson; -using CryptoExchange.Net.Objects; using System; -using System.Collections.Generic; using System.Text.Json.Serialization; namespace CryptoExchange.Net.SharedApis diff --git a/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs b/CryptoExchange.Net/Sockets/Default/CryptoExchangeWebSocketClient.cs similarity index 75% rename from CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs rename to CryptoExchange.Net/Sockets/Default/CryptoExchangeWebSocketClient.cs index 817c90e..dc38561 100644 --- a/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs +++ b/CryptoExchange.Net/Sockets/Default/CryptoExchangeWebSocketClient.cs @@ -1,23 +1,21 @@ -using CryptoExchange.Net.Interfaces; -using CryptoExchange.Net.Logging.Extensions; +using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Errors; using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.RateLimiting; +using CryptoExchange.Net.Sockets.Default.Interfaces; using Microsoft.Extensions.Logging; using System; using System.Buffers; using System.Collections.Concurrent; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net; using System.Net.Http; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.Sockets +namespace CryptoExchange.Net.Sockets.Default { /// /// A wrapper around the ClientWebSocket @@ -33,7 +31,6 @@ namespace CryptoExchange.Net.Sockets } internal static int _lastStreamId; - private static readonly object _streamIdLock = new(); private static readonly ArrayPool _receiveBufferPool = ArrayPool.Shared; private readonly AsyncResetEvent _sendEvent; @@ -42,7 +39,6 @@ namespace CryptoExchange.Net.Sockets private ClientWebSocket _socket; private CancellationTokenSource _ctsSource; - private DateTime _lastReceivedMessagesUpdate; private Task? _processTask; private Task? _closeTask; private bool _stopRequested; @@ -56,15 +52,10 @@ namespace CryptoExchange.Net.Sockets private const int _defaultReceiveBufferSize = 1048576; private const int _sendBufferSize = 4096; - /// - /// Received messages, the size and the timestamp - /// - protected readonly List _receivedMessages; - - /// - /// Received messages lock - /// - protected readonly object _receivedMessagesLock; + private int _bytesReceived = 0; + private int _prevSlotBytesReceived = 0; + private DateTime _lastBytesReceivedUpdate = DateTime.UtcNow; + private DateTime _prevSlotBytesReceivedUpdate = DateTime.UtcNow; /// /// Log @@ -96,15 +87,8 @@ namespace CryptoExchange.Net.Sockets { get { - lock (_receivedMessagesLock) - { - UpdateReceivedMessages(); - - if (_receivedMessages.Count == 0) - return 0; - - return Math.Round(_receivedMessages.Sum(v => v.Bytes) / 1000d / 3d); - } + UpdateReceivedMessages(); + return Math.Round(_prevSlotBytesReceived * (_lastBytesReceivedUpdate - _prevSlotBytesReceivedUpdate).TotalSeconds / 1000); } } @@ -137,23 +121,28 @@ namespace CryptoExchange.Net.Sockets /// public Func>? GetReconnectionUrl { get; set; } + private SocketConnection _connection; + /// /// ctor /// /// The log object to use + /// The socket connection /// The parameters for this socket - public CryptoExchangeWebSocketClient(ILogger logger, WebSocketParameters websocketParameters) + public CryptoExchangeWebSocketClient(ILogger logger, SocketConnection connection, WebSocketParameters websocketParameters) { Id = NextStreamId(); _logger = logger; + _connection = connection; Parameters = websocketParameters; - _receivedMessages = new List(); _sendEvent = new AsyncResetEvent(); _sendBuffer = new ConcurrentQueue(); _ctsSource = new CancellationTokenSource(); - _receivedMessagesLock = new object(); - _receiveBufferSize = websocketParameters.ReceiveBufferSize ?? _defaultReceiveBufferSize; + if (websocketParameters.UseUpdatedDeserialization) + _receiveBufferSize = websocketParameters.ReceiveBufferSize ?? 65536; + else + _receiveBufferSize = websocketParameters.ReceiveBufferSize ?? _defaultReceiveBufferSize; _closeSem = new SemaphoreSlim(1, 1); _socket = CreateSocket(); @@ -236,9 +225,7 @@ namespace CryptoExchange.Net.Sockets catch (Exception e) { if (ct.IsCancellationRequested) - { _logger.SocketConnectingCanceled(Id); - } else if (!_ctsSource.IsCancellationRequested) { // if _ctsSource was canceled this was already logged @@ -255,9 +242,7 @@ namespace CryptoExchange.Net.Sockets } if (_socket.HttpStatusCode == HttpStatusCode.Unauthorized) - { return new CallResult(new ServerError(new ErrorInfo(ErrorType.Unauthorized, "Server returned status code `401` when `101` was expected"))); - } #else // ClientWebSocket.HttpStatusCode is only available in .NET6+ https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket.httpstatuscode?view=net-8.0 // Try to read 429 from the message instead @@ -284,7 +269,13 @@ namespace CryptoExchange.Net.Sockets _logger.SocketStartingProcessing(Id); SetProcessState(ProcessState.Processing); var sendTask = SendLoopAsync(); - var receiveTask = ReceiveLoopAsync(); + Task receiveTask; +#if !NETSTANDARD2_0 + if (Parameters.UseUpdatedDeserialization) + receiveTask = ReceiveLoopNewAsync(); + else +#endif + receiveTask = ReceiveLoopAsync(); var timeoutTask = Parameters.Timeout != null && Parameters.Timeout > TimeSpan.FromSeconds(0) ? CheckTimeoutAsync() : Task.CompletedTask; await Task.WhenAll(sendTask, receiveTask, timeoutTask).ConfigureAwait(false); _logger.SocketFinishedProcessing(Id); @@ -482,7 +473,8 @@ namespace CryptoExchange.Net.Sockets // So socket might go to aborted state, might still be open } - _ctsSource.Cancel(); + if (!_disposed) + _ctsSource.Cancel(); } /// @@ -609,17 +601,14 @@ namespace CryptoExchange.Net.Sockets try { receiveResult = await _socket.ReceiveAsync(buffer, _ctsSource.Token).ConfigureAwait(false); - lock (_receivedMessagesLock) - _receivedMessages.Add(new ReceiveItem(DateTime.UtcNow, receiveResult.Count)); + _bytesReceived += receiveResult.Count; } catch (OperationCanceledException ex) { if (ex.InnerException?.InnerException?.Message.Contains("KeepAliveTimeout") == true) - { // Specific case that the websocket connection got closed because of a ping frame timeout // Unfortunately doesn't seem to be a nicer way to catch _logger.SocketPingTimeout(Id); - } if (_closeTask?.IsCompleted != false) _closeTask = CloseInternalAsync(); @@ -661,7 +650,8 @@ namespace CryptoExchange.Net.Sockets { // We received data, but it is not complete, write it to a memory stream for reassembling multiPartMessage = true; - _logger.SocketReceivedPartialMessage(Id, receiveResult.Count); + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.SocketReceivedPartialMessage(Id, receiveResult.Count); // Write the data to a memory stream to be reassembled later if (multipartStream == null) @@ -673,13 +663,20 @@ namespace CryptoExchange.Net.Sockets if (!multiPartMessage) { // Received a complete message and it's not multi part - _logger.SocketReceivedSingleMessage(Id, receiveResult.Count); - await ProcessData(receiveResult.MessageType, new ReadOnlyMemory(buffer.Array!, buffer.Offset, receiveResult.Count)).ConfigureAwait(false); + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.SocketReceivedSingleMessage(Id, receiveResult.Count); + + if (!Parameters.UseUpdatedDeserialization) + await ProcessData(receiveResult.MessageType, new ReadOnlyMemory(buffer.Array!, buffer.Offset, receiveResult.Count)).ConfigureAwait(false); + else + ProcessDataNew(receiveResult.MessageType, new ReadOnlySpan(buffer.Array!, buffer.Offset, receiveResult.Count)); } else { // Received the end of a multipart message, write to memory stream for reassembling - _logger.SocketReceivedPartialMessage(Id, receiveResult.Count); + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.SocketReceivedPartialMessage(Id, receiveResult.Count); + multipartStream!.Write(buffer.Array!, buffer.Offset, receiveResult.Count); } @@ -687,29 +684,179 @@ namespace CryptoExchange.Net.Sockets } } - lock (_receivedMessagesLock) - UpdateReceivedMessages(); + UpdateReceivedMessages(); if (receiveResult?.MessageType == WebSocketMessageType.Close) - { // Received close message break; - } if (receiveResult == null || _ctsSource.IsCancellationRequested) - { // Error during receiving or cancellation requested, stop. break; - } if (multiPartMessage) { // When the connection gets interrupted we might not have received a full message if (receiveResult?.EndOfMessage == true) { - _logger.SocketReassembledMessage(Id, multipartStream!.Length); + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.SocketReassembledMessage(Id, multipartStream!.Length); + // Get the underlying buffer of the memory stream holding the written data and delimit it (GetBuffer return the full array, not only the written part) - await ProcessData(receiveResult.MessageType, new ReadOnlyMemory(multipartStream.GetBuffer(), 0, (int)multipartStream.Length)).ConfigureAwait(false); + + if (!Parameters.UseUpdatedDeserialization) + await ProcessData(receiveResult.MessageType, new ReadOnlyMemory(multipartStream!.GetBuffer(), 0, (int)multipartStream.Length)).ConfigureAwait(false); + else + ProcessDataNew(receiveResult.MessageType, new ReadOnlySpan(multipartStream!.GetBuffer(), 0, (int)multipartStream.Length)); + } + else + { + _logger.SocketDiscardIncompleteMessage(Id, multipartStream!.Length); + } + } + } + } + catch (Exception e) + { + // Because this is running in a separate task and not awaited until the socket gets closed + // any exception here will crash the receive processing, but do so silently unless the socket gets stopped. + // Make sure we at least let the owner know there was an error + _logger.SocketReceiveLoopStoppedWithException(Id, e); + await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false); + if (_closeTask?.IsCompleted != false) + _closeTask = CloseInternalAsync(); + } + finally + { + _receiveBufferPool.Return(rentedBuffer, true); + _logger.SocketReceiveLoopFinished(Id); + } + } + +#if !NETSTANDARD2_0 + /// + /// Loop for receiving and reassembling data + /// + /// + private async Task ReceiveLoopNewAsync() + { + byte[] rentedBuffer = _receiveBufferPool.Rent(_receiveBufferSize); + var buffer = new Memory(rentedBuffer); + try + { + while (true) + { + if (_ctsSource.IsCancellationRequested) + break; + + MemoryStream? multipartStream = null; + ValueWebSocketReceiveResult receiveResult = new(); + bool multiPartMessage = false; + while (true) + { + try + { + receiveResult = await _socket.ReceiveAsync(buffer, _ctsSource.Token).ConfigureAwait(false); + _bytesReceived += receiveResult.Count; + } + catch (OperationCanceledException ex) + { + if (ex.InnerException?.InnerException?.Message.Contains("KeepAliveTimeout") == true) + // Specific case that the websocket connection got closed because of a ping frame timeout + // Unfortunately doesn't seem to be a nicer way to catch + _logger.SocketPingTimeout(Id); + + if (_closeTask?.IsCompleted != false) + _closeTask = CloseInternalAsync(); + + // canceled + break; + } + catch (Exception wse) + { + if (!_ctsSource.Token.IsCancellationRequested && !_stopRequested) + // Connection closed unexpectedly + await (OnError?.Invoke(wse) ?? Task.CompletedTask).ConfigureAwait(false); + + if (_closeTask?.IsCompleted != false) + _closeTask = CloseInternalAsync(); + break; + } + + if (receiveResult.MessageType == WebSocketMessageType.Close) + { + // Connection closed + if (_socket.State == WebSocketState.CloseReceived) + { + // Close received means it server initiated, we should send a confirmation and close the socket + _logger.SocketReceivedCloseMessage(Id, _socket.CloseStatus.ToString()!, _socket.CloseStatusDescription ?? string.Empty); + if (_closeTask?.IsCompleted != false) + _closeTask = CloseInternalAsync(); + } + else + { + // Means the socket is now closed and we were the one initiating it + _logger.SocketReceivedCloseConfirmation(Id, _socket.CloseStatus.ToString()!, _socket.CloseStatusDescription ?? string.Empty); + } + + break; + } + + if (!receiveResult.EndOfMessage) + { + // We received data, but it is not complete, write it to a memory stream for reassembling + multiPartMessage = true; + + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.SocketReceivedPartialMessage(Id, receiveResult.Count); + + // Write the data to a memory stream to be reassembled later + multipartStream ??= new MemoryStream(); + multipartStream.Write(buffer.Span.Slice(0, receiveResult.Count)); + } + else + { + if (!multiPartMessage) + { + // Received a complete message and it's not multi part + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.SocketReceivedSingleMessage(Id, receiveResult.Count); + + ProcessDataNew(receiveResult.MessageType, buffer.Span.Slice(0, receiveResult.Count)); + } + else + { + // Received the end of a multipart message, write to memory stream for reassembling + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.SocketReceivedPartialMessage(Id, receiveResult.Count); + + multipartStream!.Write(buffer.Span.Slice(0, receiveResult.Count)); + } + + break; + } + } + + UpdateReceivedMessages(); + + if (receiveResult.MessageType == WebSocketMessageType.Close) + // Received close message + break; + + if (_ctsSource.IsCancellationRequested) + // Error during receiving or cancellation requested, stop. + break; + + if (multiPartMessage) + { + // When the connection gets interrupted we might not have received a full message + if (receiveResult.EndOfMessage == true) + { + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.SocketReassembledMessage(Id, multipartStream!.Length); + + // Get the underlying buffer of the memory stream holding the written data and delimit it (GetBuffer return the full array, not only the written part) + ProcessDataNew(receiveResult.MessageType, new ReadOnlySpan(multipartStream!.GetBuffer(), 0, (int)multipartStream.Length)); } else { @@ -734,6 +881,19 @@ namespace CryptoExchange.Net.Sockets _logger.SocketReceiveLoopFinished(Id); } } +#endif + + /// + /// Process a stream message + /// + /// + /// + /// + protected void ProcessDataNew(WebSocketMessageType type, ReadOnlySpan data) + { + LastActionTime = DateTime.UtcNow; + _connection.HandleStreamMessage2(type, data); + } /// /// Process a stream message @@ -793,35 +953,24 @@ namespace CryptoExchange.Net.Sockets /// Get the next identifier /// /// - private static int NextStreamId() - { - lock (_streamIdLock) - { - _lastStreamId++; - return _lastStreamId; - } - } + private static int NextStreamId() => Interlocked.Increment(ref _lastStreamId); + /// /// Update the received messages list, removing messages received longer than 3s ago /// protected void UpdateReceivedMessages() { - var checkTime = DateTime.UtcNow; - if (checkTime - _lastReceivedMessagesUpdate > TimeSpan.FromSeconds(1)) - { - for (var i = 0; i < _receivedMessages.Count; i++) - { - var msg = _receivedMessages[i]; - if (checkTime - msg.Timestamp > TimeSpan.FromSeconds(3)) - { - _receivedMessages.Remove(msg); - i--; - } - } + var now = DateTime.UtcNow; + var sinceLast = now - _lastBytesReceivedUpdate; + if (sinceLast < TimeSpan.FromSeconds(3)) + return; - _lastReceivedMessagesUpdate = checkTime; - } + _prevSlotBytesReceivedUpdate = _lastBytesReceivedUpdate; + _prevSlotBytesReceived = _bytesReceived; + + _bytesReceived = 0; + _lastBytesReceivedUpdate = now; } /// @@ -886,30 +1035,4 @@ namespace CryptoExchange.Net.Sockets /// public byte[] Bytes { get; set; } } - - /// - /// Received message info - /// - public struct ReceiveItem - { - /// - /// Timestamp of the received data - /// - public DateTime Timestamp { get; set; } - /// - /// Number of bytes received - /// - public int Bytes { get; set; } - - /// - /// ctor - /// - /// - /// - public ReceiveItem(DateTime timestamp, int bytes) - { - Timestamp = timestamp; - Bytes = bytes; - } - } } diff --git a/CryptoExchange.Net/Interfaces/IWebsocket.cs b/CryptoExchange.Net/Sockets/Default/Interfaces/IWebsocket.cs similarity index 98% rename from CryptoExchange.Net/Interfaces/IWebsocket.cs rename to CryptoExchange.Net/Sockets/Default/Interfaces/IWebsocket.cs index bc360d0..3aa4f13 100644 --- a/CryptoExchange.Net/Interfaces/IWebsocket.cs +++ b/CryptoExchange.Net/Sockets/Default/Interfaces/IWebsocket.cs @@ -4,7 +4,7 @@ using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Sockets.Default.Interfaces { /// /// Websocket connection interface diff --git a/CryptoExchange.Net/Sockets/Default/Interfaces/IWebsocketFactory.cs b/CryptoExchange.Net/Sockets/Default/Interfaces/IWebsocketFactory.cs new file mode 100644 index 0000000..e0b76ae --- /dev/null +++ b/CryptoExchange.Net/Sockets/Default/Interfaces/IWebsocketFactory.cs @@ -0,0 +1,27 @@ +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Sockets.HighPerf.Interfaces; +using Microsoft.Extensions.Logging; +using System.IO.Pipelines; + +namespace CryptoExchange.Net.Sockets.Default.Interfaces +{ + /// + /// Websocket factory interface + /// + public interface IWebsocketFactory + { + /// + /// Create a websocket for an url + /// + /// The logger + /// The socket connection + /// The parameters to use for the connection + /// + IWebsocket CreateWebsocket(ILogger logger, SocketConnection connection, WebSocketParameters parameters); + + /// + /// Create high performance websocket + /// + IHighPerfWebsocket CreateHighPerfWebsocket(ILogger logger, WebSocketParameters parameters, PipeWriter pipeWriter); + } +} diff --git a/CryptoExchange.Net/Sockets/SocketConnection.cs b/CryptoExchange.Net/Sockets/Default/SocketConnection.cs similarity index 78% rename from CryptoExchange.Net/Sockets/SocketConnection.cs rename to CryptoExchange.Net/Sockets/Default/SocketConnection.cs index 6d1ee98..86808fb 100644 --- a/CryptoExchange.Net/Sockets/SocketConnection.cs +++ b/CryptoExchange.Net/Sockets/Default/SocketConnection.cs @@ -1,45 +1,85 @@ -using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Clients; +using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters; +using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Logging.Extensions; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Sockets.Default.Interfaces; +using CryptoExchange.Net.Sockets.Interfaces; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using CryptoExchange.Net.Objects; -using System.Net.WebSockets; -using CryptoExchange.Net.Objects.Sockets; using System.Diagnostics; -using CryptoExchange.Net.Clients; -using CryptoExchange.Net.Logging.Extensions; +using System.Linq; +using System.Net.WebSockets; +using System.Text; using System.Threading; +using System.Threading.Tasks; -namespace CryptoExchange.Net.Sockets +namespace CryptoExchange.Net.Sockets.Default { + /// + /// State of a the connection + /// + /// The id of the socket connection + /// The connection URI + /// Number of subscriptions on this socket + /// Socket status + /// If the connection is authenticated + /// Download speed over this socket + /// Number of non-completed queries + /// State for each subscription on this socket + public record SocketConnectionState( + int Id, + string Address, + int Subscriptions, + SocketStatus Status, + bool Authenticated, + double DownloadSpeed, + int PendingQueries, + List SubscriptionStates + ); + + /// + /// Status of the socket connection + /// + public enum SocketStatus + { + /// + /// None/Initial + /// + None, + /// + /// Connected + /// + Connected, + /// + /// Reconnecting + /// + Reconnecting, + /// + /// Resubscribing on reconnected socket + /// + Resubscribing, + /// + /// Closing + /// + Closing, + /// + /// Closed + /// + Closed, + /// + /// Disposed + /// + Disposed + } + /// /// A single socket connection to the server /// - public class SocketConnection + public class SocketConnection : ISocketConnection { - /// - /// State of a the connection - /// - /// The id of the socket connection - /// The connection URI - /// Number of subscriptions on this socket - /// Socket status - /// If the connection is authenticated - /// Download speed over this socket - /// Number of non-completed queries - /// State for each subscription on this socket - public record SocketConnectionState( - int Id, - string Address, - int Subscriptions, - SocketStatus Status, - bool Authenticated, - double DownloadSpeed, - int PendingQueries, - List SubscriptionStates - ); /// /// Connection lost event @@ -88,7 +128,7 @@ namespace CryptoExchange.Net.Sockets { get { - lock(_listenersLock) + lock (_listenersLock) return _listeners.OfType().Count(h => h.UserSubscription); } } @@ -100,7 +140,7 @@ namespace CryptoExchange.Net.Sockets { get { - lock(_listenersLock) + lock (_listenersLock) return _listeners.OfType().Where(h => h.UserSubscription).ToArray(); } } @@ -110,6 +150,9 @@ namespace CryptoExchange.Net.Sockets /// public bool Authenticated { get; set; } + /// + public bool HasAuthenticatedSubscription => Subscriptions.Any(x => x.Authenticated); + /// /// If connection is made /// @@ -144,7 +187,7 @@ namespace CryptoExchange.Net.Sockets /// Tag for identification /// public string Tag { get; set; } - + /// /// Additional properties for this connection /// @@ -162,7 +205,7 @@ namespace CryptoExchange.Net.Sockets { _pausedActivity = value; _logger.ActivityPaused(SocketId, value); - if(_pausedActivity) _ = Task.Run(() => ActivityPaused?.Invoke()); + if (_pausedActivity) _ = Task.Run(() => ActivityPaused?.Invoke()); else _ = Task.Run(() => ActivityUnpaused?.Invoke()); } } @@ -215,7 +258,11 @@ namespace CryptoExchange.Net.Sockets } private bool _pausedActivity; - private readonly object _listenersLock; +#if NET9_0_OR_GREATER + private readonly Lock _listenersLock = new Lock(); +#else + private readonly object _listenersLock = new object(); +#endif private readonly List _listeners; private readonly ILogger _logger; private SocketStatus _status; @@ -224,6 +271,9 @@ namespace CryptoExchange.Net.Sockets private IByteMessageAccessor? _stringMessageAccessor; private IByteMessageAccessor? _byteMessageAccessor; + private ISocketMessageHandler? _byteMessageConverter; + private ISocketMessageHandler? _textMessageConverter; + /// /// The task that is sending periodic data on the websocket. Can be used for sending Ping messages every x seconds or similar. Not necessary. /// @@ -247,18 +297,16 @@ namespace CryptoExchange.Net.Sockets /// /// New socket connection /// - /// The logger - /// The api client - /// The socket - /// - public SocketConnection(ILogger logger, SocketApiClient apiClient, IWebsocket socket, string tag) + public SocketConnection(ILogger logger, IWebsocketFactory socketFactory, WebSocketParameters parameters, SocketApiClient apiClient, string tag) { _logger = logger; ApiClient = apiClient; Tag = tag; Properties = new Dictionary(); - _socket = socket; + _socket = socketFactory.CreateWebsocket(logger, this, parameters); + _logger.SocketCreatedForAddress(_socket.Id, parameters.Uri.ToString()); + _socket.OnStreamMessage += HandleStreamMessage; _socket.OnRequestSent += HandleRequestSentAsync; _socket.OnRequestRateLimited += HandleRequestRateLimitedAsync; @@ -270,7 +318,6 @@ namespace CryptoExchange.Net.Sockets _socket.OnError += HandleErrorAsync; _socket.GetReconnectionUrl = GetReconnectionUrlAsync; - _listenersLock = new object(); _listeners = new List(); _serializer = apiClient.CreateSerializer(); @@ -294,10 +341,16 @@ namespace CryptoExchange.Net.Sockets Status = SocketStatus.Closed; Authenticated = false; + if (ApiClient._socketConnections.ContainsKey(SocketId)) + ApiClient._socketConnections.TryRemove(SocketId, out _); + lock (_listenersLock) { foreach (var subscription in _listeners.OfType().Where(l => l.UserSubscription && !l.IsClosingConnection)) + { + subscription.IsClosingConnection = true; subscription.Reset(); + } foreach (var query in _listeners.OfType().ToList()) { @@ -382,7 +435,7 @@ namespace CryptoExchange.Net.Sockets }); } } - catch(Exception ex) + catch (Exception ex) { _logger.UnknownExceptionWhileProcessingReconnection(SocketId, ex); _ = _socket.ReconnectAsync().ConfigureAwait(false); @@ -432,7 +485,7 @@ namespace CryptoExchange.Net.Sockets /// protected async virtual Task HandleConnectRateLimitedAsync() { - if (ConnectRateLimitedAsync is not null) + if (ConnectRateLimitedAsync is not null) await ConnectRateLimitedAsync().ConfigureAwait(false); } @@ -449,12 +502,166 @@ namespace CryptoExchange.Net.Sockets } if (query == null) - return Task.CompletedTask; + return Task.CompletedTask; query.IsSend(query.RequestTimeout ?? ApiClient.ClientOptions.RequestTimeout); return Task.CompletedTask; } + /// + /// Handle a message + /// + protected internal virtual void HandleStreamMessage2(WebSocketMessageType type, ReadOnlySpan data) + { + var receiveTime = DateTime.UtcNow; + + // 1. Decrypt/Preprocess if necessary + data = ApiClient.PreprocessStreamMessage(this, type, data); + + ISocketMessageHandler messageConverter; + if (type == WebSocketMessageType.Binary) + messageConverter = _byteMessageConverter ??= ApiClient.CreateMessageConverter(type); + else + messageConverter = _textMessageConverter ??= ApiClient.CreateMessageConverter(type); + + string? originalData = null; + if (ApiClient.ApiOptions.OutputOriginalData ?? ApiClient.ClientOptions.OutputOriginalData) + { +#if NETSTANDARD2_0 + originalData = Encoding.UTF8.GetString(data.ToArray()); +#else + originalData = Encoding.UTF8.GetString(data); +#endif + + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.ReceivedData(SocketId, originalData); + } + + var typeIdentifier = messageConverter.GetTypeIdentifier(data, type); + if (typeIdentifier == null) + { + // Both deserialization type and identifier null, can't process + _logger.LogWarning("Failed to evaluate message. Data: {Message}", Encoding.UTF8.GetString(data.ToArray())); + return; + } + + Type? deserializationType = null; + lock (_listenersLock) + { + foreach (var subscription in _listeners) + { + foreach (var route in subscription.MessageRouter.Routes) + { + if (!route.TypeIdentifier.Equals(typeIdentifier, StringComparison.Ordinal)) + continue; + + deserializationType = route.DeserializationType; + break; + } + + if (deserializationType != null) + break; + } + } + + if (deserializationType == null) + { + // No handler found for identifier either, can't process + _logger.LogWarning("Failed to determine message type for identifier {Identifier}. Data: {Message}", typeIdentifier, Encoding.UTF8.GetString(data.ToArray())); + return; + } + + + object result; + try + { + if (deserializationType == typeof(string)) + { +#if NETSTANDARD2_0 + result = Encoding.UTF8.GetString(data.ToArray()); +#else + result = Encoding.UTF8.GetString(data); +#endif + } + else + { + result = messageConverter.Deserialize(data, deserializationType); + } + } + catch(Exception ex) + { + _logger.LogWarning(ex, "Deserialization failed. Data: {Message}", Encoding.UTF8.GetString(data.ToArray())); + return; + } + + if (result == null) + { + // Deserialize error + _logger.LogWarning("Deserialization returned null. Data: {Message}", Encoding.UTF8.GetString(data.ToArray())); + return; + } + + var topicFilter = messageConverter.GetTopicFilter(result); + + bool processed = false; + lock (_listenersLock) + { + var currentCount = _listeners.Count; + for(var i = 0; i < _listeners.Count; i++) + { + if (_listeners.Count != currentCount) + { + // Possible a query added or removed. If added it's not a problem, if removed it is + if (_listeners.Count < currentCount) + throw new Exception("Listeners list adjusted, can't continue processing"); + } + + var processor = _listeners[i]; + bool isQuery = false; + Query? query = null; + if (processor is Query cquery) + { + isQuery = true; + query = cquery; + } + + var complete = false; + + foreach (var route in processor.MessageRouter.Routes) + { + if (route.TypeIdentifier != typeIdentifier) + continue; + + if (topicFilter == null + || route.TopicFilter == null + || route.TopicFilter.Equals(topicFilter, StringComparison.Ordinal)) + { + if (isQuery && query!.Completed) + continue; + + processed = true; + processor.Handle(this, receiveTime, originalData, result, route); + + if (isQuery && !route.MultipleReaders) + { + complete = true; + break; + } + } + } + + if (complete) + break; + } + } + + if (!processed) + { + _logger.ReceivedMessageNotMatchedToAnyListener(SocketId, topicFilter!, + string.Join(",", _listeners.Select(x => string.Join(",", x.MessageRouter.Routes.Select(x => x.TopicFilter != null ? string.Join(",", x.TopicFilter) : "[null]"))))); + } + } + /// /// Handle a message /// @@ -506,18 +713,18 @@ namespace CryptoExchange.Net.Sockets var totalUserTime = 0; List localListeners; - lock(_listenersLock) + lock (_listenersLock) localListeners = _listeners.ToList(); - foreach(var processor in localListeners) + foreach (var processor in localListeners) { - foreach(var listener in processor.MessageMatcher.GetHandlerLinks(listenId)) + foreach (var listener in processor.MessageMatcher.GetHandlerLinks(listenId)) { processed = true; _logger.ProcessorMatched(SocketId, listener.ToString(), listenId); // 4. Determine the type to deserialize to for this processor - var messageType = listener.GetDeserializationType(accessor); + var messageType = listener.DeserializationType; if (messageType == null) { _logger.ReceivedMessageNotRecognized(SocketId, processor.Id); @@ -554,7 +761,7 @@ namespace CryptoExchange.Net.Sockets try { var innerSw = Stopwatch.StartNew(); - await processor.Handle(this, new DataEvent(deserialized, null, null, originalData, receiveTime, null), listener).ConfigureAwait(false); + processor.Handle(this, receiveTime, originalData, deserialized, listener); if (processor is Query query && query.RequiredResponses != 1) _logger.LogDebug($"[Sckt {SocketId}] [Req {query.Id}] responses: {query.CurrentResponses}/{query.RequiredResponses}"); totalUserTime += (int)innerSw.ElapsedMilliseconds; @@ -630,8 +837,8 @@ namespace CryptoExchange.Net.Sockets if (Status == SocketStatus.Closed || Status == SocketStatus.Disposed) return; - if (ApiClient.socketConnections.ContainsKey(SocketId)) - ApiClient.socketConnections.TryRemove(SocketId, out _); + if (ApiClient._socketConnections.ContainsKey(SocketId)) + ApiClient._socketConnections.TryRemove(SocketId, out _); lock (_listenersLock) { @@ -676,7 +883,7 @@ namespace CryptoExchange.Net.Sockets bool shouldCloseConnection; lock (_listenersLock) shouldCloseConnection = _listeners.OfType().All(r => !r.UserSubscription || r.Status == SubscriptionStatus.Closing || r.Status == SubscriptionStatus.Closed) && !DedicatedRequestConnection.IsDedicatedRequestConnection; - + if (!anyDuplicateSubscription) { bool needUnsub; @@ -778,12 +985,11 @@ namespace CryptoExchange.Net.Sockets /// Send a query request and wait for an answer /// /// Query to send - /// Wait event for when the socket message handler can continue /// Cancellation token /// - public virtual async Task SendAndWaitQueryAsync(Query query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default) + public virtual async Task SendAndWaitQueryAsync(Query query, CancellationToken ct = default) { - await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false); + await SendAndWaitIntAsync(query, ct).ConfigureAwait(false); return query.Result ?? new CallResult(new TimeoutError()); } @@ -792,22 +998,20 @@ namespace CryptoExchange.Net.Sockets /// /// Expected result type /// Query to send - /// Wait event for when the socket message handler can continue /// Cancellation token /// - public virtual async Task> SendAndWaitQueryAsync(Query query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default) + public virtual async Task> SendAndWaitQueryAsync(Query query, CancellationToken ct = default) { - await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false); + await SendAndWaitIntAsync(query, ct).ConfigureAwait(false); return query.TypedResult ?? new CallResult(new TimeoutError()); } - private async Task SendAndWaitIntAsync(Query query, AsyncResetEvent? continueEvent, CancellationToken ct = default) + private async Task SendAndWaitIntAsync(Query query, CancellationToken ct = default) { - lock(_listenersLock) + lock (_listenersLock) _listeners.Add(query); - query.ContinueAwaiter = continueEvent; - var sendResult = Send(query.Id, query.Request, query.Weight); + var sendResult = await SendAsync(query.Id, query.Request, query.Weight).ConfigureAwait(false); if (!sendResult) { query.Fail(sendResult.Error!); @@ -855,19 +1059,19 @@ namespace CryptoExchange.Net.Sockets /// The request id /// The object to send /// The weight of the message - public virtual CallResult Send(int requestId, T obj, int weight) + public virtual ValueTask SendAsync(int requestId, T obj, int weight) { if (_serializer is IByteMessageSerializer byteSerializer) { - return SendBytes(requestId, byteSerializer.Serialize(obj), weight); + return SendBytesAsync(requestId, byteSerializer.Serialize(obj), weight); } else if (_serializer is IStringMessageSerializer stringSerializer) { if (obj is string str) - return Send(requestId, str, weight); + return SendStringAsync(requestId, str, weight); str = stringSerializer.Serialize(obj); - return Send(requestId, str, weight); + return SendAsync(requestId, str, weight); } throw new Exception("Unknown serializer when sending message"); @@ -879,7 +1083,7 @@ namespace CryptoExchange.Net.Sockets /// The data to send /// The weight of the message /// The id of the request - public virtual CallResult SendBytes(int requestId, byte[] data, int weight) + public virtual async ValueTask SendBytesAsync(int requestId, byte[] data, int weight) { if (ApiClient.MessageSendSizeLimit != null && data.Length > ApiClient.MessageSendSizeLimit.Value) { @@ -914,7 +1118,7 @@ namespace CryptoExchange.Net.Sockets /// The data to send /// The weight of the message /// The id of the request - public virtual CallResult Send(int requestId, string data, int weight) + public virtual async ValueTask SendStringAsync(int requestId, string data, int weight) { if (ApiClient.MessageSendSizeLimit != null && data.Length > ApiClient.MessageSendSizeLimit.Value) { @@ -937,7 +1141,7 @@ namespace CryptoExchange.Net.Sockets return CallResult.SuccessResult; } - catch(Exception ex) + catch (Exception ex) { return new CallResult(new WebError("Failed to send message: " + ex.Message, exception: ex)); } @@ -966,7 +1170,7 @@ namespace CryptoExchange.Net.Sockets lock (_listenersLock) { anyAuthenticated = _listeners.OfType().Any(s => s.Authenticated) - || (DedicatedRequestConnection.IsDedicatedRequestConnection && DedicatedRequestConnection.Authenticated); + || DedicatedRequestConnection.IsDedicatedRequestConnection && DedicatedRequestConnection.Authenticated; } if (anyAuthenticated) @@ -1021,15 +1225,13 @@ namespace CryptoExchange.Net.Sockets subscription.Status = SubscriptionStatus.Subscribed; continue; } + subQuery.OnComplete = () => + { + subscription.Status = subQuery.Result!.Success ? SubscriptionStatus.Subscribed : SubscriptionStatus.Pending; + subscription.HandleSubQueryResponse(subQuery.Response); + }; - var waitEvent = new AsyncResetEvent(false); - taskList.Add(SendAndWaitQueryAsync(subQuery, waitEvent).ContinueWith((r) => - { - subscription.Status = r.Result.Success ? SubscriptionStatus.Subscribed: SubscriptionStatus.Pending; - subscription.HandleSubQueryResponse(subQuery.Response!); - waitEvent.Set(); - return r.Result; - })); + taskList.Add(SendAndWaitQueryAsync(subQuery)); } await Task.WhenAll(taskList).ConfigureAwait(false); @@ -1119,40 +1321,6 @@ namespace CryptoExchange.Net.Sockets }); } - /// - /// Status of the socket connection - /// - public enum SocketStatus - { - /// - /// None/Initial - /// - None, - /// - /// Connected - /// - Connected, - /// - /// Reconnecting - /// - Reconnecting, - /// - /// Resubscribing on reconnected socket - /// - Resubscribing, - /// - /// Closing - /// - Closing, - /// - /// Closed - /// - Closed, - /// - /// Disposed - /// - Disposed - } } } diff --git a/CryptoExchange.Net/Sockets/Subscription.cs b/CryptoExchange.Net/Sockets/Default/Subscription.cs similarity index 80% rename from CryptoExchange.Net/Sockets/Subscription.cs rename to CryptoExchange.Net/Sockets/Default/Subscription.cs index 90318f1..c41b084 100644 --- a/CryptoExchange.Net/Sockets/Subscription.cs +++ b/CryptoExchange.Net/Sockets/Default/Subscription.cs @@ -1,14 +1,12 @@ using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Sockets.Interfaces; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.Sockets +namespace CryptoExchange.Net.Sockets.Default { /// /// Socket subscription @@ -75,7 +73,12 @@ namespace CryptoExchange.Net.Sockets /// /// Matcher for this subscription /// - public MessageMatcher MessageMatcher { get; set; } = null!; + public MessageMatcher MessageMatcher { get; set; } + + /// + /// Router for this subscription + /// + public MessageRouter MessageRouter { get; set; } /// /// Cancellation token registration @@ -106,10 +109,20 @@ namespace CryptoExchange.Net.Sockets /// public Query? UnsubscriptionQuery { get; private set; } + /// + /// The number of individual streams in this subscription + /// + public int IndividualSubscriptionCount { get; set; } = 1; + /// /// ctor /// - public Subscription(ILogger logger, bool authenticated, bool userSubscription = true) +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. + public Subscription( +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. + ILogger logger, + bool authenticated, + bool userSubscription = true) { _logger = logger; Authenticated = authenticated; @@ -137,7 +150,7 @@ namespace CryptoExchange.Net.Sockets /// Handle a subscription query response /// /// - public virtual void HandleSubQueryResponse(object message) { } + public virtual void HandleSubQueryResponse(object? message) { } /// /// Handle an unsubscription query response @@ -167,11 +180,21 @@ namespace CryptoExchange.Net.Sockets /// /// Handle an update message /// - public Task Handle(SocketConnection connection, DataEvent message, MessageHandlerLink matcher) + public CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object data, MessageHandlerLink matcher) { ConnectionInvocations++; TotalInvocations++; - return Task.FromResult(matcher.Handle(connection, message)); + return matcher.Handle(connection, receiveTime, originalData, data); + } + + /// + /// Handle an update message + /// + public CallResult? Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object data, MessageRoute route) + { + ConnectionInvocations++; + TotalInvocations++; + return route.Handle(connection, receiveTime, originalData, data); } /// @@ -220,38 +243,4 @@ namespace CryptoExchange.Net.Sockets return new SubscriptionState(Id, Status, TotalInvocations, MessageMatcher); } } - - /// - public abstract class Subscription : Subscription - { - /// - /// ctor - /// - /// - /// - protected Subscription(ILogger logger, bool authenticated) : base(logger, authenticated) - { - } - - /// - public override void HandleSubQueryResponse(object message) - => HandleSubQueryResponse((TSubResponse)message); - - /// - /// Handle a subscription query response - /// - /// - public virtual void HandleSubQueryResponse(TSubResponse message) { } - - /// - public override void HandleUnsubQueryResponse(object message) - => HandleUnsubQueryResponse((TUnsubResponse)message); - - /// - /// Handle an unsubscription query response - /// - /// - public virtual void HandleUnsubQueryResponse(TUnsubResponse message) { } - - } } diff --git a/CryptoExchange.Net/Sockets/SystemSubscription.cs b/CryptoExchange.Net/Sockets/Default/SystemSubscription.cs similarity index 81% rename from CryptoExchange.Net/Sockets/SystemSubscription.cs rename to CryptoExchange.Net/Sockets/Default/SystemSubscription.cs index bef4ec0..ea1379a 100644 --- a/CryptoExchange.Net/Sockets/SystemSubscription.cs +++ b/CryptoExchange.Net/Sockets/Default/SystemSubscription.cs @@ -1,10 +1,7 @@ -using CryptoExchange.Net.Interfaces; -using CryptoExchange.Net.Objects; -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Objects; using Microsoft.Extensions.Logging; -using System; -namespace CryptoExchange.Net.Sockets +namespace CryptoExchange.Net.Sockets.Default { /// /// A system subscription @@ -19,6 +16,8 @@ namespace CryptoExchange.Net.Sockets public SystemSubscription(ILogger logger, bool authenticated = false) : base(logger, authenticated, false) { Status = SubscriptionStatus.Subscribed; + + IndividualSubscriptionCount = 0; } /// diff --git a/CryptoExchange.Net/Sockets/Default/WebsocketFactory.cs b/CryptoExchange.Net/Sockets/Default/WebsocketFactory.cs new file mode 100644 index 0000000..a046c00 --- /dev/null +++ b/CryptoExchange.Net/Sockets/Default/WebsocketFactory.cs @@ -0,0 +1,26 @@ +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Sockets.Default.Interfaces; +using CryptoExchange.Net.Sockets.HighPerf; +using CryptoExchange.Net.Sockets.HighPerf.Interfaces; +using Microsoft.Extensions.Logging; +using System.IO.Pipelines; + +namespace CryptoExchange.Net.Sockets.Default +{ + /// + /// Default websocket factory implementation + /// + public class WebsocketFactory : IWebsocketFactory + { + /// + public IWebsocket CreateWebsocket(ILogger logger, SocketConnection connection, WebSocketParameters parameters) + { + return new CryptoExchangeWebSocketClient(logger, connection, parameters); + } + /// + public IHighPerfWebsocket CreateHighPerfWebsocket(ILogger logger, WebSocketParameters parameters, PipeWriter pipeWriter) + { + return new HighPerfWebSocketClient(logger, parameters, pipeWriter); + } + } +} diff --git a/CryptoExchange.Net/Sockets/HighPerf/HighPerfJsonSocketConnection.cs b/CryptoExchange.Net/Sockets/HighPerf/HighPerfJsonSocketConnection.cs new file mode 100644 index 0000000..3d76ac2 --- /dev/null +++ b/CryptoExchange.Net/Sockets/HighPerf/HighPerfJsonSocketConnection.cs @@ -0,0 +1,55 @@ +using CryptoExchange.Net.Clients; +using CryptoExchange.Net.Exceptions; +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Sockets.Default.Interfaces; +using Microsoft.Extensions.Logging; +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace CryptoExchange.Net.Sockets.HighPerf +{ + /// + /// A single socket connection focused on performance expecting JSON data + /// + /// The type of updates this connection produces + public class HighPerfJsonSocketConnection : HighPerfSocketConnection + { + private JsonSerializerOptions _jsonOptions; + + /// + /// ctor + /// + public HighPerfJsonSocketConnection( + ILogger logger, + IWebsocketFactory socketFactory, + WebSocketParameters parameters, + SocketApiClient apiClient, + JsonSerializerOptions serializerOptions, + string tag) + : base(logger, socketFactory, parameters, apiClient, tag) + { + _jsonOptions = serializerOptions; + } + + /// + protected override async Task ProcessAsync(CancellationToken ct) + { + 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. + await foreach (var update in JsonSerializer.DeserializeAsyncEnumerable(_pipe.Reader, true, _jsonOptions, ct).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 + { + foreach (var sub in _typedSubscriptions) + DelegateToSubscription(_typedSubscriptions[0], update!); + } + } + catch (OperationCanceledException) { } + } + + } +} diff --git a/CryptoExchange.Net/Sockets/HighPerf/HighPerfJsonSocketConnectionFactory.cs b/CryptoExchange.Net/Sockets/HighPerf/HighPerfJsonSocketConnectionFactory.cs new file mode 100644 index 0000000..df53e5c --- /dev/null +++ b/CryptoExchange.Net/Sockets/HighPerf/HighPerfJsonSocketConnectionFactory.cs @@ -0,0 +1,30 @@ +using CryptoExchange.Net.Clients; +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Sockets.Default.Interfaces; +using CryptoExchange.Net.Sockets.HighPerf.Interfaces; +using Microsoft.Extensions.Logging; +using System.Text.Json; + +namespace CryptoExchange.Net.Sockets.HighPerf +{ + /// + public class HighPerfJsonSocketConnectionFactory : IHighPerfConnectionFactory + { + private readonly JsonSerializerOptions _options; + + /// + /// ctor + /// + public HighPerfJsonSocketConnectionFactory(JsonSerializerOptions options) + { + _options = options; + } + + /// + public HighPerfSocketConnection CreateHighPerfConnection( + ILogger logger, IWebsocketFactory factory, WebSocketParameters parameters, SocketApiClient client, string address) + { + return new HighPerfJsonSocketConnection(logger, factory, parameters, client, _options, address); + } + } +} diff --git a/CryptoExchange.Net/Sockets/HighPerf/HighPerfSocketConnection.cs b/CryptoExchange.Net/Sockets/HighPerf/HighPerfSocketConnection.cs new file mode 100644 index 0000000..5d4f77e --- /dev/null +++ b/CryptoExchange.Net/Sockets/HighPerf/HighPerfSocketConnection.cs @@ -0,0 +1,453 @@ +using CryptoExchange.Net.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using CryptoExchange.Net.Objects; +using System.Net.WebSockets; +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Clients; +using CryptoExchange.Net.Logging.Extensions; +using System.Threading; +using System.IO.Pipelines; +using CryptoExchange.Net.Sockets.Interfaces; +using CryptoExchange.Net.Sockets.Default; +using CryptoExchange.Net.Sockets.HighPerf.Interfaces; +using CryptoExchange.Net.Sockets.Default.Interfaces; + +namespace CryptoExchange.Net.Sockets.HighPerf +{ + /// + /// A single socket connection focused on performance + /// + public abstract class HighPerfSocketConnection : ISocketConnection + { + /// + /// Connection closed and no reconnect is happening + /// + public event Action? ConnectionClosed; + + /// + public bool Authenticated { get; set; } = false; + + /// + public bool HasAuthenticatedSubscription => false; + + /// + /// The amount of subscriptions on this connection + /// + public int UserSubscriptionCount => Subscriptions.Length; + + /// + /// Get a copy of the current message subscriptions + /// + public abstract HighPerfSubscription[] Subscriptions { get; } + + /// + /// If connection is made + /// + public bool Connected => _socket.IsOpen; + + /// + /// The unique ID of the socket + /// + public int SocketId => _socket.Id; + + /// + /// The connection uri + /// + public Uri ConnectionUri => _socket.Uri; + + /// + /// The API client the connection is for + /// + public SocketApiClient ApiClient { get; set; } + + /// + /// Tag for identification + /// + public string Tag { get; set; } + + /// + /// Additional properties for this connection + /// + public Dictionary Properties { get; set; } + + /// + /// Status of the socket connection + /// + public SocketStatus Status + { + get => _status; + private set + { + if (_status == value) + return; + + var oldStatus = _status; + _status = value; + _logger.SocketStatusChanged(SocketId, oldStatus, value); + } + } + + /// + /// Logger + /// + protected readonly ILogger _logger; + + private readonly IMessageSerializer _serializer; + private readonly CancellationTokenSource _cts = new CancellationTokenSource(); + private SocketStatus _status; + private Task? _processTask; + + /// + /// The pipe the websocket will write to + /// + protected readonly Pipe _pipe; + /// + /// Update type + /// + public abstract Type UpdateType { get; } + + /// + /// The task that is sending periodic data on the websocket. Can be used for sending Ping messages every x seconds or similar. Not necessary. + /// + protected Task? periodicTask; + + /// + /// Wait event for the periodicTask + /// + protected AsyncResetEvent? periodicEvent; + + /// + /// The underlying websocket + /// + private readonly IHighPerfWebsocket _socket; + + /// + /// New socket connection + /// + public HighPerfSocketConnection(ILogger logger, IWebsocketFactory socketFactory, WebSocketParameters parameters, SocketApiClient apiClient, string tag) + { + _logger = logger; + _pipe = new Pipe(); + ApiClient = apiClient; + Tag = tag; + Properties = new Dictionary(); + + _socket = socketFactory.CreateHighPerfWebsocket(logger, parameters, _pipe.Writer); + _logger.SocketCreatedForAddress(_socket.Id, parameters.Uri.ToString()); + + _socket.OnOpen += HandleOpenAsync; + _socket.OnClose += HandleCloseAsync; + + _socket.OnError += HandleErrorAsync; + + _serializer = apiClient.CreateSerializer(); + } + + /// + /// Process messages from the pipe + /// + protected abstract Task ProcessAsync(CancellationToken ct); + + /// + /// Handler for a socket opening + /// + protected virtual Task HandleOpenAsync() + { + Status = SocketStatus.Connected; + return Task.CompletedTask; + } + + /// + /// Handler for a socket closing without reconnect + /// + protected virtual async Task HandleCloseAsync() + { + Status = SocketStatus.Closed; + _cts.CancelAfter(TimeSpan.FromSeconds(1)); // Cancel after 1 second to make sure we process pending messages from the pipe + + if (ApiClient._highPerfSocketConnections.ContainsKey(SocketId)) + ApiClient._highPerfSocketConnections.TryRemove(SocketId, out _); + + await _processTask!.ConfigureAwait(false); + + _ = Task.Run(() => ConnectionClosed?.Invoke()); + } + + /// + /// Handler for an error on a websocket + /// + /// The exception + protected virtual Task HandleErrorAsync(Exception e) + { + if (e is WebSocketException wse) + _logger.WebSocketErrorCodeAndDetails(SocketId, wse.WebSocketErrorCode, wse.Message, wse); + else + _logger.WebSocketError(SocketId, e.Message, e); + + return Task.CompletedTask; + } + + /// + /// Connect the websocket + /// + /// + public async Task ConnectAsync(CancellationToken ct) + { + var result = await _socket.ConnectAsync(ct).ConfigureAwait(false); + if (result.Success) + _processTask = ProcessAsync(_cts.Token); + + return result; + } + + /// + /// Close the connection + /// + /// + public async Task CloseAsync() + { + if (Status == SocketStatus.Closed || Status == SocketStatus.Disposed) + return; + + if (ApiClient._highPerfSocketConnections.ContainsKey(SocketId)) + ApiClient._highPerfSocketConnections.TryRemove(SocketId, out _); + + foreach (var subscription in Subscriptions) + { + if (subscription.CancellationTokenRegistration.HasValue) + subscription.CancellationTokenRegistration.Value.Dispose(); + } + + await _socket.CloseAsync().ConfigureAwait(false); + _socket.Dispose(); + } + + /// + /// Dispose the connection + /// + public void Dispose() + { + Status = SocketStatus.Disposed; + periodicEvent?.Set(); + periodicEvent?.Dispose(); + _socket.Dispose(); + } + + /// + /// Send data over the websocket connection + /// + /// The type of the object to send + /// The object to send + public virtual ValueTask SendAsync(T obj) + { + if (_serializer is IByteMessageSerializer byteSerializer) + return SendBytesAsync(byteSerializer.Serialize(obj)); + else if (_serializer is IStringMessageSerializer stringSerializer) + { + if (obj is string str) + return SendStringAsync(str); + + str = stringSerializer.Serialize(obj); + return SendStringAsync(str); + } + + throw new Exception("Unknown serializer when sending message"); + } + + /// + /// Send byte data over the websocket connection + /// + /// The data to send + public virtual async ValueTask SendBytesAsync(byte[] data) + { + if (ApiClient.MessageSendSizeLimit != null && data.Length > ApiClient.MessageSendSizeLimit.Value) + { + var info = $"Message to send exceeds the max server message size ({data.Length} vs {ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit"; + _logger.LogWarning("[Sckt {SocketId}] {Info}", SocketId, info); + return new CallResult(new InvalidOperationError(info)); + } + + if (!_socket.IsOpen) + { + _logger.LogWarning("[Sckt {SocketId}] Request failed to send, socket no longer open", SocketId); + return new CallResult(new WebError("Failed to send message, socket no longer open")); + } + + try + { + if (!await _socket.SendAsync(data).ConfigureAwait(false)) + return new CallResult(new WebError("Failed to send message, connection not open")); + + return CallResult.SuccessResult; + } + catch (Exception ex) + { + return new CallResult(new WebError("Failed to send message: " + ex.Message, exception: ex)); + } + } + + /// + /// Send string data over the websocket connection + /// + /// The data to send + public virtual async ValueTask SendStringAsync(string data) + { + if (ApiClient.MessageSendSizeLimit != null && data.Length > ApiClient.MessageSendSizeLimit.Value) + { + var info = $"Message to send exceeds the max server message size ({data.Length} vs {ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit"; + _logger.LogWarning("[Sckt {SocketId}] {Info}", SocketId, info); + return new CallResult(new InvalidOperationError(info)); + } + + if (!_socket.IsOpen) + { + _logger.LogWarning("[Sckt {SocketId}] Request failed to send, socket no longer open", SocketId); + return new CallResult(new WebError("Failed to send message, socket no longer open")); + } + + try + { + if (!await _socket.SendAsync(data).ConfigureAwait(false)) + return new CallResult(new WebError("Failed to send message, connection not open")); + + return CallResult.SuccessResult; + } + catch (Exception ex) + { + return new CallResult(new WebError("Failed to send message: " + ex.Message, exception: ex)); + } + } + + /// + /// Periodically sends data over a socket connection + /// + /// Identifier for the periodic send + /// How often + /// Method returning the query to send + public virtual void QueryPeriodic(string identifier, TimeSpan interval, Func queryDelegate) + { + if (queryDelegate == null) + throw new ArgumentNullException(nameof(queryDelegate)); + + periodicEvent = new AsyncResetEvent(); + periodicTask = Task.Run(async () => + { + while (Status != SocketStatus.Disposed + && Status != SocketStatus.Closed + && Status != SocketStatus.Closing) + { + await periodicEvent.WaitAsync(interval).ConfigureAwait(false); + if (Status == SocketStatus.Disposed + || Status == SocketStatus.Closed + || Status == SocketStatus.Closing) + { + break; + } + + if (!Connected) + continue; + + var query = queryDelegate(this); + if (query == null) + continue; + + _logger.SendingPeriodic(SocketId, identifier); + + try + { + var result = await SendAsync(query).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.PeriodicSendFailed(SocketId, identifier, ex.Message, ex); + } + } + }); + } + } + + /// + public abstract class HighPerfSocketConnection : HighPerfSocketConnection + { + /// + /// Lock for listener access + /// +#if NET9_0_OR_GREATER + protected readonly Lock _listenersLock = new Lock(); +#else + protected readonly object _listenersLock = new object(); +#endif + + /// + /// Subscriptions + /// + protected readonly List> _typedSubscriptions; + + /// + public override HighPerfSubscription[] Subscriptions + { + get + { + lock (_listenersLock) + return _typedSubscriptions.Select(x => (HighPerfSubscription)x).ToArray(); + } + } + + /// + public override Type UpdateType => typeof(T); + + /// + /// ctor + /// + public HighPerfSocketConnection(ILogger logger, IWebsocketFactory socketFactory, WebSocketParameters parameters, SocketApiClient apiClient, string tag) + : base(logger, socketFactory, parameters, apiClient, tag) + { + _typedSubscriptions = new List>(); + } + + /// + /// Add a new subscription + /// + public bool AddSubscription(HighPerfSubscription subscription) + { + if (Status != SocketStatus.None && Status != SocketStatus.Connected) + return false; + + _typedSubscriptions.Add(subscription); + + _logger.AddingNewSubscription(SocketId, subscription.Id, UserSubscriptionCount); + return true; + } + + /// + /// Remove a subscription + /// + /// + public void RemoveSubscription(HighPerfSubscription subscription) + { + lock (_listenersLock) + _typedSubscriptions.Remove(subscription); + } + + /// + /// Delegate the update to the listeners + /// + protected void DelegateToSubscription(HighPerfSubscription subscription, T update) + { + try + { + subscription.HandleAsync(update!); + } + catch (Exception ex) + { + subscription.InvokeExceptionHandler(ex); + _logger.UserMessageProcessingFailed(SocketId, ex.Message, ex); + } + } + } +} + diff --git a/CryptoExchange.Net/Sockets/HighPerf/HighPerfSubscription.cs b/CryptoExchange.Net/Sockets/HighPerf/HighPerfSubscription.cs new file mode 100644 index 0000000..874f5e4 --- /dev/null +++ b/CryptoExchange.Net/Sockets/HighPerf/HighPerfSubscription.cs @@ -0,0 +1,92 @@ +using System; +using System.Threading; + +namespace CryptoExchange.Net.Sockets.HighPerf +{ + /// + /// Socket subscription + /// + public abstract class HighPerfSubscription + { + /// + /// Subscription id + /// + public int Id { get; set; } + + /// + /// Total amount of invocations + /// + public int TotalInvocations { get; set; } + + /// + /// Cancellation token registration + /// + public CancellationTokenRegistration? CancellationTokenRegistration { get; set; } + + /// + /// Exception event + /// + public event Action? Exception; + + /// + /// The subscribe query for this subscription + /// + public object? SubscriptionQuery { get; private set; } + + /// + /// ctor + /// + public HighPerfSubscription() + { + Id = ExchangeHelpers.NextId(); + } + + /// + /// Create a new subscription query + /// + public object? CreateSubscriptionQuery(HighPerfSocketConnection connection) + { + var query = GetSubQuery(connection); + SubscriptionQuery = query; + return query; + } + + /// + /// Get the subscribe query to send when subscribing + /// + /// + protected abstract object? GetSubQuery(HighPerfSocketConnection connection); + + /// + /// Invoke the exception event + /// + /// + public void InvokeExceptionHandler(Exception e) + { + Exception?.Invoke(e); + } + } + + /// + public abstract class HighPerfSubscription : HighPerfSubscription + { + private Action _handler; + + /// + /// ctor + /// + protected HighPerfSubscription(Action handler) : base() + { + _handler = handler; + } + + /// + /// Handle an update + /// + public void HandleAsync(TUpdateType update) + { + TotalInvocations++; + _handler.Invoke(update); + } + } +} diff --git a/CryptoExchange.Net/Sockets/HighPerf/HighPerfWebSocketClient.cs b/CryptoExchange.Net/Sockets/HighPerf/HighPerfWebSocketClient.cs new file mode 100644 index 0000000..e2a7907 --- /dev/null +++ b/CryptoExchange.Net/Sockets/HighPerf/HighPerfWebSocketClient.cs @@ -0,0 +1,535 @@ +using CryptoExchange.Net.Logging.Extensions; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Errors; +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Sockets.HighPerf.Interfaces; +using Microsoft.Extensions.Logging; +using System; +using System.Buffers; +using System.IO.Pipelines; +using System.Net; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; + +namespace CryptoExchange.Net.Sockets.HighPerf +{ + /// + /// A high performance websocket client implementation + /// + public class HighPerfWebSocketClient : IHighPerfWebsocket + { + private ClientWebSocket? _socket; + +#if NETSTANDARD2_0 + private static readonly ArrayPool _receiveBufferPool = ArrayPool.Shared; +#endif + + private readonly SemaphoreSlim _closeSem; + + private CancellationTokenSource _ctsSource; + private Task? _processTask; + private Task? _closeTask; + private bool _stopRequested; + private bool _disposed; + private bool _processing; + private readonly int _receiveBufferSize; + private readonly PipeWriter _pipeWriter; + + private const int _defaultReceiveBufferSize = 4096; + private const int _sendBufferSize = 4096; + + /// + /// Log + /// + protected ILogger _logger; + + /// + public int Id { get; } + + /// + public WebSocketParameters Parameters { get; } + + /// + public Uri Uri => Parameters.Uri; + + /// + public virtual bool IsClosed => _socket == null || _socket?.State == WebSocketState.Closed; + + /// + public virtual bool IsOpen => _socket?.State == WebSocketState.Open && !_ctsSource.IsCancellationRequested; + + /// + public event Func? OnClose; + + /// + public event Func? OnError; + + /// + public event Func? OnOpen; + + /// + /// ctor + /// + public HighPerfWebSocketClient(ILogger logger, WebSocketParameters websocketParameters, PipeWriter pipeWriter) + { + Id = ExchangeHelpers.NextId(); + _logger = logger; + + Parameters = websocketParameters; + _ctsSource = new CancellationTokenSource(); + _receiveBufferSize = websocketParameters.ReceiveBufferSize ?? _defaultReceiveBufferSize; + + _pipeWriter = pipeWriter; + _closeSem = new SemaphoreSlim(1, 1); + } + + /// + public virtual async Task ConnectAsync(CancellationToken ct) + { + var connectResult = await ConnectInternalAsync(ct).ConfigureAwait(false); + if (!connectResult) + return connectResult; + + await (OnOpen?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); + _processTask = ProcessAsync(); + return connectResult; + } + + /// + /// Create the socket object + /// + private ClientWebSocket CreateSocket() + { + var cookieContainer = new CookieContainer(); + foreach (var cookie in Parameters.Cookies) + cookieContainer.Add(new Cookie(cookie.Key, cookie.Value)); + + var socket = new ClientWebSocket(); + try + { + socket.Options.Cookies = cookieContainer; + foreach (var header in Parameters.Headers) + socket.Options.SetRequestHeader(header.Key, header.Value); + socket.Options.KeepAliveInterval = Parameters.KeepAliveInterval ?? TimeSpan.Zero; + socket.Options.SetBuffer(_receiveBufferSize, _sendBufferSize); + if (Parameters.Proxy != null) + SetProxy(socket, Parameters.Proxy); + +#if NET6_0_OR_GREATER + socket.Options.CollectHttpResponseDetails = true; +#endif +#if NET9_0_OR_GREATER + socket.Options.KeepAliveTimeout = Parameters.KeepAliveTimeout ?? TimeSpan.FromSeconds(10); +#endif + } + catch (PlatformNotSupportedException) + { + // Options are not supported on certain platforms (WebAssembly for instance) + // best we can do it try to connect without setting options. + } + + return socket; + } + + private async Task ConnectInternalAsync(CancellationToken ct) + { + _logger.SocketConnecting(Id); + try + { + using CancellationTokenSource tcs = new(TimeSpan.FromSeconds(10)); + using var linked = CancellationTokenSource.CreateLinkedTokenSource(tcs.Token, _ctsSource.Token, ct); + _socket = CreateSocket(); + await _socket.ConnectAsync(Uri, linked.Token).ConfigureAwait(false); + } + catch (Exception e) + { + if (ct.IsCancellationRequested) + { + _logger.SocketConnectingCanceled(Id); + } + else if (!_ctsSource.IsCancellationRequested) + { + // if _ctsSource was canceled this was already logged + _logger.SocketConnectionFailed(Id, e.Message, e); + } + + if (e is WebSocketException we) + { +#if (NET6_0_OR_GREATER) + if (_socket!.HttpStatusCode == HttpStatusCode.TooManyRequests) + return new CallResult(new ServerRateLimitError(we.Message, we)); + + if (_socket.HttpStatusCode == HttpStatusCode.Unauthorized) + return new CallResult(new ServerError(new ErrorInfo(ErrorType.Unauthorized, "Server returned status code `401` when `101` was expected"))); +#else + // ClientWebSocket.HttpStatusCode is only available in .NET6+ https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket.httpstatuscode?view=net-8.0 + // Try to read 429 from the message instead + if (we.Message.Contains("429")) + return new CallResult(new ServerRateLimitError(we.Message, we)); +#endif + } + + return new CallResult(new CantConnectError(e)); + } + + _logger.SocketConnected(Id, Uri); + return CallResult.SuccessResult; + } + + /// + private async Task ProcessAsync() + { + _logger.SocketStartingProcessing(Id); + _processing = true; + await ReceiveLoopAsync().ConfigureAwait(false); + _processing = false; + _logger.SocketFinishedProcessing(Id); + + while (_closeTask == null) + await Task.Delay(50).ConfigureAwait(false); + + await _closeTask.ConfigureAwait(false); + await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); + _logger.SocketClosed(Id); + } + + /// + public virtual ValueTask SendAsync(string data) + { + var bytes = Parameters.Encoding.GetBytes(data); + return SendAsync(bytes, WebSocketMessageType.Text); + } + + /// + public virtual async ValueTask SendAsync(byte[] data, WebSocketMessageType type = WebSocketMessageType.Binary) + { + if (_ctsSource.IsCancellationRequested || !_processing) + return false; + + try + { + await _socket!.SendAsync(new ArraySegment(data, 0, data.Length), type, true, _ctsSource.Token).ConfigureAwait(false); + return true; + } + catch (OperationCanceledException) + { + // canceled + return false; + } + catch (Exception ioe) + { + // Connection closed unexpectedly, .NET framework + await (OnError?.Invoke(ioe) ?? Task.CompletedTask).ConfigureAwait(false); + if (_closeTask?.IsCompleted != false) + _closeTask = CloseInternalAsync(); + return false; + } + } + + /// + public virtual async Task CloseAsync() + { + await _closeSem.WaitAsync().ConfigureAwait(false); + _stopRequested = true; + + try + { + if (_closeTask?.IsCompleted == false) + { + _logger.SocketCloseAsyncWaitingForExistingCloseTask(Id); + await _closeTask.ConfigureAwait(false); + return; + } + + if (!IsOpen) + { + _logger.SocketCloseAsyncSocketNotOpen(Id); + return; + } + + _logger.SocketClosing(Id); + _closeTask = CloseInternalAsync(); + } + finally + { + _closeSem.Release(); + } + + await _closeTask.ConfigureAwait(false); + if(_processTask != null) + await _processTask.ConfigureAwait(false); + } + + /// + /// Internal close method + /// + /// + private async Task CloseInternalAsync() + { + if (_disposed) + return; + + try + { + if (_socket!.State == WebSocketState.CloseReceived) + { + await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false); + } + else if (_socket.State == WebSocketState.Open) + { + await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false); + + var startWait = DateTime.UtcNow; + while (_processing && _socket.State != WebSocketState.Closed && _socket.State != WebSocketState.Aborted) + { + // Wait until we receive close confirmation + await Task.Delay(10).ConfigureAwait(false); + if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(1)) + break; // Wait for max 1 second, then just abort the connection + } + } + } + catch (Exception) + { + // Can sometimes throw an exception when socket is in aborted state due to timing + // Websocket is set to Aborted state when the cancelation token is set during SendAsync/ReceiveAsync + // So socket might go to aborted state, might still be open + } + + if (!_disposed) + _ctsSource.Cancel(); + } + + /// + /// Dispose the socket + /// + public void Dispose() + { + if (_disposed) + return; + + if (_ctsSource?.IsCancellationRequested == false) + _ctsSource.Cancel(); + + _logger.SocketDisposing(Id); + _disposed = true; + _socket?.Dispose(); + _ctsSource?.Dispose(); + _logger.SocketDisposed(Id); + } + +#if NETSTANDARD2_1 || NET8_0_OR_GREATER + private async Task ReceiveLoopAsync() + { + Exception? exitException = null; + + try + { + while (true) + { + if (_ctsSource.IsCancellationRequested) + break; + + ValueWebSocketReceiveResult receiveResult; + + try + { + receiveResult = await _socket!.ReceiveAsync(_pipeWriter.GetMemory(_receiveBufferSize), _ctsSource.Token).ConfigureAwait(false); + + // Advance the writer to communicate which part of the memory was written + _pipeWriter.Advance(receiveResult.Count); + } + catch (OperationCanceledException ex) + { + if (ex.InnerException?.InnerException?.Message.Contains("KeepAliveTimeout") == true) + // Specific case that the websocket connection got closed because of a ping frame timeout + // Unfortunately doesn't seem to be a nicer way to catch + _logger.SocketPingTimeout(Id); + + if (_closeTask?.IsCompleted != false) + _closeTask = CloseInternalAsync(); + + exitException = ex; + break; + } + catch (Exception wse) + { + if (!_ctsSource.Token.IsCancellationRequested && !_stopRequested) + // Connection closed unexpectedly + await (OnError?.Invoke(wse) ?? Task.CompletedTask).ConfigureAwait(false); + + if (_closeTask?.IsCompleted != false) + _closeTask = CloseInternalAsync(); + + exitException = wse; + break; + } + + if (receiveResult.EndOfMessage) + { + // Flush the full message + var flushResult = await _pipeWriter.FlushAsync().ConfigureAwait(false); + if (flushResult.IsCompleted) + { + // Flush indicated that the reader is no longer listening, so we should stop writing + if (_closeTask?.IsCompleted != false) + _closeTask = CloseInternalAsync(); + + break; + } + } + + if (receiveResult.MessageType == WebSocketMessageType.Close) + { + // Connection closed + if (_socket.State == WebSocketState.CloseReceived) + { + // Close received means it's server initiated, we should send a confirmation and close the socket + _logger.SocketReceivedCloseMessage(Id, _socket.CloseStatus?.ToString() ?? string.Empty, _socket.CloseStatusDescription ?? string.Empty); + if (_closeTask?.IsCompleted != false) + _closeTask = CloseInternalAsync(); + } + else + { + // Means the socket is now closed and we were the one initiating it + _logger.SocketReceivedCloseConfirmation(Id, _socket.CloseStatus?.ToString() ?? string.Empty, _socket.CloseStatusDescription ?? string.Empty); + } + + break; + } + } + } + catch (Exception e) + { + // Because this is running in a separate task and not awaited until the socket gets closed + // any exception here will crash the receive processing, but do so silently unless the socket gets stopped. + // Make sure we at least let the owner know there was an error + _logger.SocketReceiveLoopStoppedWithException(Id, e); + + exitException = e; + await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false); + if (_closeTask?.IsCompleted != false) + _closeTask = CloseInternalAsync(); + } + finally + { + await _pipeWriter.CompleteAsync(exitException).ConfigureAwait(false); + _logger.SocketReceiveLoopFinished(Id); + } + } +#else + + private async Task ReceiveLoopAsync() + { + byte[] rentedBuffer = _receiveBufferPool.Rent(_receiveBufferSize); + var buffer = new ArraySegment(rentedBuffer); + Exception? exitException = null; + + try + { + while (true) + { + if (_ctsSource.IsCancellationRequested) + break; + + WebSocketReceiveResult? receiveResult = null; + try + { + receiveResult = await _socket!.ReceiveAsync(buffer, _ctsSource.Token).ConfigureAwait(false); + } + catch (OperationCanceledException ex) + { + if (ex.InnerException?.InnerException?.Message.Contains("KeepAliveTimeout") == true) + // Specific case that the websocket connection got closed because of a ping frame timeout + // Unfortunately doesn't seem to be a nicer way to catch + _logger.SocketPingTimeout(Id); + + if (_closeTask?.IsCompleted != false) + _closeTask = CloseInternalAsync(); + + exitException = ex; + break; + } + catch (Exception wse) + { + if (!_ctsSource.Token.IsCancellationRequested && !_stopRequested) + // Connection closed unexpectedly + await (OnError?.Invoke(wse) ?? Task.CompletedTask).ConfigureAwait(false); + + if (_closeTask?.IsCompleted != false) + _closeTask = CloseInternalAsync(); + + exitException = wse; + break; + } + + if (receiveResult.Count > 0) + await _pipeWriter.WriteAsync(buffer.AsMemory(0, receiveResult.Count)).ConfigureAwait(false); + + if (receiveResult.MessageType == WebSocketMessageType.Close) + { + // Connection closed + if (_socket.State == WebSocketState.CloseReceived) + { + // Close received means it server initiated, we should send a confirmation and close the socket + _logger.SocketReceivedCloseMessage(Id, receiveResult.CloseStatus.ToString()!, receiveResult.CloseStatusDescription ?? string.Empty); + if (_closeTask?.IsCompleted != false) + _closeTask = CloseInternalAsync(); + } + else + { + // Means the socket is now closed and we were the one initiating it + _logger.SocketReceivedCloseConfirmation(Id, receiveResult.CloseStatus.ToString()!, receiveResult.CloseStatusDescription ?? string.Empty); + } + + break; + } + } + + } + catch (Exception e) + { + // Because this is running in a separate task and not awaited until the socket gets closed + // any exception here will crash the receive processing, but do so silently unless the socket gets stopped. + // Make sure we at least let the owner know there was an error + _logger.SocketReceiveLoopStoppedWithException(Id, e); + + exitException = e; + await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false); + if (_closeTask?.IsCompleted != false) + _closeTask = CloseInternalAsync(); + } + finally + { + await _pipeWriter.CompleteAsync(exitException).ConfigureAwait(false); + + _receiveBufferPool.Return(rentedBuffer, true); + _logger.SocketReceiveLoopFinished(Id); + } + } +#endif + + /// + /// Set proxy on socket + /// + /// + /// + /// + protected virtual void SetProxy(ClientWebSocket socket, ApiProxy proxy) + { + if (!Uri.TryCreate($"{proxy.Host}:{proxy.Port}", UriKind.Absolute, out var uri)) + throw new ArgumentException("Proxy settings invalid, {proxy.Host}:{proxy.Port} not a valid URI", nameof(proxy)); + + socket.Options.Proxy = uri?.Scheme == null + ? socket.Options.Proxy = new WebProxy(proxy.Host, proxy.Port) + : socket.Options.Proxy = new WebProxy + { + Address = uri + }; + + if (proxy.Login != null) + socket.Options.Proxy.Credentials = new NetworkCredential(proxy.Login, proxy.Password); + } + } +} \ No newline at end of file diff --git a/CryptoExchange.Net/Sockets/HighPerf/Interfaces/IHighPerfConnectionFactory.cs b/CryptoExchange.Net/Sockets/HighPerf/Interfaces/IHighPerfConnectionFactory.cs new file mode 100644 index 0000000..406328c --- /dev/null +++ b/CryptoExchange.Net/Sockets/HighPerf/Interfaces/IHighPerfConnectionFactory.cs @@ -0,0 +1,19 @@ +using CryptoExchange.Net.Clients; +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Sockets.Default.Interfaces; +using Microsoft.Extensions.Logging; + +namespace CryptoExchange.Net.Sockets.HighPerf.Interfaces +{ + /// + /// Factory for creating connections + /// + public interface IHighPerfConnectionFactory + { + /// + /// Create a new websocket connection + /// + HighPerfSocketConnection CreateHighPerfConnection( + ILogger logger, IWebsocketFactory factory, WebSocketParameters parameters, SocketApiClient client, string address); + } +} diff --git a/CryptoExchange.Net/Sockets/HighPerf/Interfaces/IHighPerfWebsocket.cs b/CryptoExchange.Net/Sockets/HighPerf/Interfaces/IHighPerfWebsocket.cs new file mode 100644 index 0000000..cd81fce --- /dev/null +++ b/CryptoExchange.Net/Sockets/HighPerf/Interfaces/IHighPerfWebsocket.cs @@ -0,0 +1,62 @@ +using CryptoExchange.Net.Objects; +using System; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; + +namespace CryptoExchange.Net.Sockets.HighPerf.Interfaces +{ + /// + /// Websocket connection interface + /// + public interface IHighPerfWebsocket : IDisposable + { + /// + /// Websocket closed event + /// + event Func OnClose; + /// + /// Websocket error event + /// + event Func OnError; + /// + /// Websocket opened event + /// + event Func OnOpen; + + /// + /// Unique id for this socket + /// + int Id { get; } + /// + /// The uri the socket connects to + /// + Uri Uri { get; } + /// + /// Whether the socket connection is closed + /// + bool IsClosed { get; } + /// + /// Whether the socket connection is open + /// + bool IsOpen { get; } + /// + /// Connect the socket + /// + /// + Task ConnectAsync(CancellationToken ct); + /// + /// Send string data + /// + ValueTask SendAsync(string data); + /// + /// Send byte data + /// + ValueTask SendAsync(byte[] data, WebSocketMessageType type = WebSocketMessageType.Binary); + /// + /// Close the connection + /// + /// + Task CloseAsync(); + } +} diff --git a/CryptoExchange.Net/Interfaces/IMessageProcessor.cs b/CryptoExchange.Net/Sockets/Interfaces/IMessageProcessor.cs similarity index 52% rename from CryptoExchange.Net/Interfaces/IMessageProcessor.cs rename to CryptoExchange.Net/Sockets/Interfaces/IMessageProcessor.cs index 749a04f..c98f846 100644 --- a/CryptoExchange.Net/Interfaces/IMessageProcessor.cs +++ b/CryptoExchange.Net/Sockets/Interfaces/IMessageProcessor.cs @@ -1,11 +1,9 @@ -using CryptoExchange.Net.Objects; -using CryptoExchange.Net.Objects.Sockets; -using CryptoExchange.Net.Sockets; +using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Sockets.Default; using System; -using System.Collections.Generic; -using System.Threading.Tasks; -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Sockets.Interfaces { /// /// Message processor @@ -21,9 +19,17 @@ namespace CryptoExchange.Net.Interfaces /// public MessageMatcher MessageMatcher { get; } /// + /// The message router for this processor + /// + public MessageRouter MessageRouter { get; } + /// /// Handle a message /// - Task Handle(SocketConnection connection, DataEvent message, MessageHandlerLink matchedHandler); + CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object result, MessageHandlerLink matchedHandler); + /// + /// Handle a message + /// + CallResult? Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object result, MessageRoute route); /// /// Deserialize a message into object of type /// diff --git a/CryptoExchange.Net/Sockets/Interfaces/ISocketConnection.cs b/CryptoExchange.Net/Sockets/Interfaces/ISocketConnection.cs new file mode 100644 index 0000000..4291be9 --- /dev/null +++ b/CryptoExchange.Net/Sockets/Interfaces/ISocketConnection.cs @@ -0,0 +1,61 @@ +using CryptoExchange.Net.Clients; +using CryptoExchange.Net.Objects; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace CryptoExchange.Net.Sockets.Interfaces +{ + /// + /// Socket connection + /// + public interface ISocketConnection + { + /// + /// The API client the connection belongs to + /// + SocketApiClient ApiClient { get; set; } + /// + /// Whether the connection has been authenticated + /// + bool Authenticated { get; set; } + /// + /// Is there a subscription which requires authentication on this connection + /// + bool HasAuthenticatedSubscription { get; } + /// + /// Whether the connection is established + /// + bool Connected { get; } + /// + /// Connection URI + /// + Uri ConnectionUri { get; } + /// + /// Id + /// + int SocketId { get; } + /// + /// Tag + /// + string Tag { get; set; } + /// + /// Closed event + /// + + event Action? ConnectionClosed; + /// + /// Connect the websocket + /// + Task ConnectAsync(CancellationToken ct); + /// + /// Close the connection + /// + /// + Task CloseAsync(); + /// + /// Dispose + /// + void Dispose(); + } +} \ No newline at end of file diff --git a/CryptoExchange.Net/Sockets/MessageMatcher.cs b/CryptoExchange.Net/Sockets/MessageMatcher.cs index 2ca7107..b208f44 100644 --- a/CryptoExchange.Net/Sockets/MessageMatcher.cs +++ b/CryptoExchange.Net/Sockets/MessageMatcher.cs @@ -1,11 +1,8 @@ -using CryptoExchange.Net.Interfaces; -using CryptoExchange.Net.Objects; -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Sockets.Default; using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace CryptoExchange.Net.Sockets { @@ -45,15 +42,23 @@ namespace CryptoExchange.Net.Sockets /// /// Create message matcher /// - public static MessageMatcher Create(string value) + public static MessageMatcher Create(string value) { - return new MessageMatcher(new MessageHandlerLink(MessageLinkType.Full, value, (con, msg) => new CallResult(default, msg.OriginalData, null))); + return new MessageMatcher(new MessageHandlerLink(MessageLinkType.Full, value, (con, receiveTime, originalData, msg) => new CallResult(default, null, null))); } /// /// Create message matcher /// - public static MessageMatcher Create(string value, Func, CallResult> handler) + public static MessageMatcher Create(string value) + { + return new MessageMatcher(new MessageHandlerLink(MessageLinkType.Full, value, (con, receiveTime, originalData, msg) => new CallResult(default, null, null))); + } + + /// + /// Create message matcher + /// + public static MessageMatcher Create(string value, Func handler) { return new MessageMatcher(new MessageHandlerLink(MessageLinkType.Full, value, handler)); } @@ -61,7 +66,7 @@ namespace CryptoExchange.Net.Sockets /// /// Create message matcher /// - public static MessageMatcher Create(IEnumerable values, Func, CallResult> handler) + public static MessageMatcher Create(IEnumerable values, Func handler) { return new MessageMatcher(values.Select(x => new MessageHandlerLink(MessageLinkType.Full, x, handler)).ToArray()); } @@ -69,7 +74,7 @@ namespace CryptoExchange.Net.Sockets /// /// Create message matcher /// - public static MessageMatcher Create(MessageLinkType type, string value, Func, CallResult> handler) + public static MessageMatcher Create(MessageLinkType type, string value, Func handler) { return new MessageMatcher(new MessageHandlerLink(type, value, handler)); } @@ -90,7 +95,7 @@ namespace CryptoExchange.Net.Sockets /// /// Get any handler links matching with the listen id /// - public List GetHandlerLinks(string listenId) => HandlerLinks.Where(x => x.Check(listenId)).ToList(); + public IEnumerable GetHandlerLinks(string listenId) => HandlerLinks.Where(x => x.Check(listenId)); /// public override string ToString() => string.Join(",", HandlerLinks.Select(x => x.ToString())); @@ -112,7 +117,7 @@ namespace CryptoExchange.Net.Sockets /// /// Deserialization type /// - public abstract Type GetDeserializationType(IMessageAccessor accessor); + public abstract Type DeserializationType { get; } /// /// ctor @@ -137,7 +142,7 @@ namespace CryptoExchange.Net.Sockets /// /// Message handler /// - public abstract CallResult Handle(SocketConnection connection, DataEvent message); + public abstract CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object data); /// public override string ToString() => $"{Type} match for \"{Value}\""; @@ -148,15 +153,15 @@ namespace CryptoExchange.Net.Sockets /// public class MessageHandlerLink: MessageHandlerLink { - private Func, CallResult> _handler; + private Func _handler; /// - public override Type GetDeserializationType(IMessageAccessor accessor) => typeof(TServer); + public override Type DeserializationType => typeof(TServer); /// /// ctor /// - public MessageHandlerLink(string value, Func, CallResult> handler) + public MessageHandlerLink(string value, Func handler) : this(MessageLinkType.Full, value, handler) { } @@ -164,7 +169,7 @@ namespace CryptoExchange.Net.Sockets /// /// ctor /// - public MessageHandlerLink(MessageLinkType type, string value, Func, CallResult> handler) + public MessageHandlerLink(MessageLinkType type, string value, Func handler) : base(type, value) { _handler = handler; @@ -172,9 +177,9 @@ namespace CryptoExchange.Net.Sockets /// - public override CallResult Handle(SocketConnection connection, DataEvent message) + public override CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object data) { - return _handler(connection, message.As((TServer)message.Data)); + return _handler(connection, receiveTime, originalData, (TServer)data); } } } diff --git a/CryptoExchange.Net/Sockets/MessageRouter.cs b/CryptoExchange.Net/Sockets/MessageRouter.cs new file mode 100644 index 0000000..4c29763 --- /dev/null +++ b/CryptoExchange.Net/Sockets/MessageRouter.cs @@ -0,0 +1,268 @@ +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Sockets.Default; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace CryptoExchange.Net.Sockets +{ + /// + /// Message router + /// + public class MessageRouter + { + /// + /// The routes registered for this router + /// + public MessageRoute[] Routes { get; } + + /// + /// ctor + /// + private MessageRouter(params MessageRoute[] routes) + { + Routes = routes; + } + + /// + /// Create message router without specific message handler + /// + public static MessageRouter CreateWithoutHandler(string typeIdentifier, bool multipleReaders = false) + { + return new MessageRouter(new MessageRoute(typeIdentifier, (string?)null, (con, receiveTime, originalData, msg) => new CallResult(default, null, null), multipleReaders)); + } + + /// + /// Create message router without specific message handler + /// + public static MessageRouter CreateWithoutHandler(string typeIdentifier, string topicFilter, bool multipleReaders = false) + { + return new MessageRouter(new MessageRoute(typeIdentifier, topicFilter, (con, receiveTime, originalData, msg) => new CallResult(default, null, null), multipleReaders)); + } + + /// + /// Create message router without topic filter + /// + public static MessageRouter CreateWithoutTopicFilter(IEnumerable values, Func handler, bool multipleReaders = false) + { + return new MessageRouter(values.Select(x => new MessageRoute(x, null, handler, multipleReaders)).ToArray()); + } + + /// + /// Create message router without topic filter + /// + public static MessageRouter CreateWithoutTopicFilter(string typeIdentifier, Func handler, bool multipleReaders = false) + { + return new MessageRouter(new MessageRoute(typeIdentifier, null, handler, multipleReaders)); + } + + /// + /// Create message router with topic filter + /// + public static MessageRouter CreateWithTopicFilter(string typeIdentifier, string topicFilter, Func handler, bool multipleReaders = false) + { + return new MessageRouter(new MessageRoute(typeIdentifier, topicFilter, handler, multipleReaders)); + } + + /// + /// Create message router with topic filter + /// + public static MessageRouter CreateWithTopicFilter(IEnumerable typeIdentifiers, string topicFilter, Func handler, bool multipleReaders = false) + { + var routes = new List(); + foreach (var type in typeIdentifiers) + routes.Add(new MessageRoute(type, topicFilter, handler, multipleReaders)); + + return new MessageRouter(routes.ToArray()); + } + + /// + /// Create message router with topic filter + /// + public static MessageRouter CreateWithTopicFilters(string typeIdentifier, IEnumerable topicFilters, Func handler, bool multipleReaders = false) + { + var routes = new List(); + foreach (var filter in topicFilters) + routes.Add(new MessageRoute(typeIdentifier, filter, handler, multipleReaders)); + + return new MessageRouter(routes.ToArray()); + } + + /// + /// Create message router with topic filter + /// + public static MessageRouter CreateWithTopicFilters(IEnumerable typeIdentifiers, IEnumerable topicFilters, Func handler, bool multipleReaders = false) + { + var routes = new List(); + foreach (var type in typeIdentifiers) + { + foreach (var filter in topicFilters) + routes.Add(new MessageRoute(type, filter, handler, multipleReaders)); + } + + return new MessageRouter(routes.ToArray()); + } + + /// + /// Create message router with optional topic filter + /// + public static MessageRouter CreateWithOptionalTopicFilter(string typeIdentifier, string? topicFilter, Func handler, bool multipleReaders = false) + { + return new MessageRouter(new MessageRoute(typeIdentifier, topicFilter, handler, multipleReaders)); + } + + /// + /// Create message router with optional topic filter + /// + public static MessageRouter CreateWithOptionalTopicFilters(string typeIdentifier, IEnumerable? topicFilters, Func handler, bool multipleReaders = false) + { + var routes = new List(); + if (topicFilters?.Count() > 0) + { + foreach (var filter in topicFilters) + routes.Add(new MessageRoute(typeIdentifier, filter, handler, multipleReaders)); + } + else + { + routes.Add(new MessageRoute(typeIdentifier, null, handler, multipleReaders)); + } + + return new MessageRouter(routes.ToArray()); + } + + /// + /// Create message router with optional topic filter + /// + public static MessageRouter CreateWithOptionalTopicFilters(IEnumerable typeIdentifiers, IEnumerable? topicFilters, Func handler, bool multipleReaders = false) + { + var routes = new List(); + foreach (var typeIdentifier in typeIdentifiers) + { + if (topicFilters?.Count() > 0) + { + foreach (var filter in topicFilters) + routes.Add(new MessageRoute(typeIdentifier, filter, handler, multipleReaders)); + } + else + { + routes.Add(new MessageRoute(typeIdentifier, null, handler, multipleReaders)); + } + } + + return new MessageRouter(routes.ToArray()); + } + + /// + /// Create message matcher with specific routes + /// + public static MessageRouter Create(params MessageRoute[] routes) + { + return new MessageRouter(routes); + } + + /// + /// Whether this matcher contains a specific link + /// + public bool ContainsCheck(MessageRoute route) => Routes.Any(x => x.TypeIdentifier == route.TypeIdentifier && x.TopicFilter == route.TopicFilter); + } + + /// + /// Message route + /// + public abstract class MessageRoute + { + /// + /// Type identifier + /// + public string TypeIdentifier { get; set; } + /// + /// Optional topic filter + /// + public string? TopicFilter { get; set; } + + /// + /// Whether responses to this route might be read by multiple listeners + /// + public bool MultipleReaders { get; set; } = false; + + /// + /// Deserialization type + /// + public abstract Type DeserializationType { get; } + + /// + /// ctor + /// + public MessageRoute(string typeIdentifier, string? topicFilter) + { + TypeIdentifier = typeIdentifier; + TopicFilter = topicFilter; + } + + /// + /// Message handler + /// + public abstract CallResult? Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object data); + } + + /// + /// Message route + /// + public class MessageRoute : MessageRoute + { + private Func _handler; + + /// + public override Type DeserializationType { get; } = typeof(TMessage); + + /// + /// ctor + /// + internal MessageRoute(string typeIdentifier, string? topicFilter, Func handler, bool multipleReaders = false) + : base(typeIdentifier, topicFilter) + { + _handler = handler; + MultipleReaders = multipleReaders; + } + + /// + /// Create route without topic filter + /// + public static MessageRoute CreateWithoutTopicFilter(string typeIdentifier, Func handler, bool multipleReaders = false) + { + return new MessageRoute(typeIdentifier, null, handler) + { + MultipleReaders = multipleReaders + }; + } + + /// + /// Create route with optional topic filter + /// + public static MessageRoute CreateWithOptionalTopicFilter(string typeIdentifier, string? topicFilter, Func handler, bool multipleReaders = false) + { + return new MessageRoute(typeIdentifier, topicFilter, handler) + { + MultipleReaders = multipleReaders + }; + } + + /// + /// Create route with topic filter + /// + public static MessageRoute CreateWithTopicFilter(string typeIdentifier, string topicFilter, Func handler, bool multipleReaders = false) + { + return new MessageRoute(typeIdentifier, topicFilter, handler) + { + MultipleReaders = multipleReaders + }; + } + + /// + public override CallResult? Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object data) + { + return _handler(connection, receiveTime, originalData, (TMessage)data); + } + } + +} diff --git a/CryptoExchange.Net/Sockets/PeriodicTaskRegistration.cs b/CryptoExchange.Net/Sockets/PeriodicTaskRegistration.cs index 9c532bb..57b5027 100644 --- a/CryptoExchange.Net/Sockets/PeriodicTaskRegistration.cs +++ b/CryptoExchange.Net/Sockets/PeriodicTaskRegistration.cs @@ -1,4 +1,6 @@ using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Sockets.Default; +using CryptoExchange.Net.Sockets.Interfaces; using System; namespace CryptoExchange.Net.Sockets @@ -19,7 +21,7 @@ namespace CryptoExchange.Net.Sockets /// /// Delegate for getting the query /// - public Func QueryDelegate { get; set; } = null!; + public Func QueryDelegate { get; set; } = null!; /// /// Callback after query /// diff --git a/CryptoExchange.Net/Sockets/Query.cs b/CryptoExchange.Net/Sockets/Query.cs index 9d38368..3796768 100644 --- a/CryptoExchange.Net/Sockets/Query.cs +++ b/CryptoExchange.Net/Sockets/Query.cs @@ -1,9 +1,8 @@ using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Sockets.Default; +using CryptoExchange.Net.Sockets.Interfaces; using System; -using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -60,15 +59,15 @@ namespace CryptoExchange.Net.Sockets /// public object? Response { get; set; } - /// - /// Wait event for the calling message processing thread - /// - public AsyncResetEvent? ContinueAwaiter { get; set; } - /// /// Matcher for this query /// - public MessageMatcher MessageMatcher { get; set; } = null!; + public MessageMatcher MessageMatcher { get; set; } + + /// + /// Router for this query + /// + public MessageRouter MessageRouter { get; set; } /// /// The query request object @@ -100,13 +99,17 @@ namespace CryptoExchange.Net.Sockets /// protected CancellationTokenSource? _cts; + /// + /// On complete callback + /// + public Action? OnComplete { get; set; } + /// /// ctor /// - /// - /// - /// +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. public Query(object request, bool authenticated, int weight = 1) +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. { _event = new AsyncResetEvent(false, false); @@ -160,7 +163,12 @@ namespace CryptoExchange.Net.Sockets /// /// Handle a response message /// - public abstract Task Handle(SocketConnection connection, DataEvent message, MessageHandlerLink check); + public abstract CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object message, MessageHandlerLink check); + + /// + /// Handle a response message + /// + public abstract CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object message, MessageRoute route); } @@ -181,30 +189,59 @@ namespace CryptoExchange.Net.Sockets /// /// /// - protected Query(object request, bool authenticated, int weight = 1) : base(request, authenticated, weight) + protected Query( + object request, + bool authenticated, + int weight = 1) + : base(request, authenticated, weight) { } /// - public override async Task Handle(SocketConnection connection, DataEvent message, MessageHandlerLink check) + public override CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object message, MessageRoute route) + { + CurrentResponses++; + if (CurrentResponses == RequiredResponses) + Response = message; + + if (Result?.Success != false) + { + // If an error result is already set don't override that + Result = route.Handle(connection, receiveTime, originalData, message); + if (Result == null) + // Null from Handle means it wasn't actually for this query + CurrentResponses -= 1; + } + + if (CurrentResponses == RequiredResponses) + { + Completed = true; + _event.Set(); + OnComplete?.Invoke(); + } + + return Result ?? CallResult.SuccessResult; + } + + /// + public override CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object message, MessageHandlerLink check) { if (!PreCheckMessage(connection, message)) return CallResult.SuccessResult; CurrentResponses++; if (CurrentResponses == RequiredResponses) - Response = message.Data; + Response = message; if (Result?.Success != false) // If an error result is already set don't override that - Result = check.Handle(connection, message); + Result = check.Handle(connection, receiveTime, originalData, message); if (CurrentResponses == RequiredResponses) { Completed = true; _event.Set(); - if (ContinueAwaiter != null) - await ContinueAwaiter.WaitAsync().ConfigureAwait(false); + OnComplete?.Invoke(); } return Result; @@ -213,7 +250,7 @@ namespace CryptoExchange.Net.Sockets /// /// Validate if a message is actually processable by this query /// - public virtual bool PreCheckMessage(SocketConnection connection, DataEvent message) => true; + public virtual bool PreCheckMessage(SocketConnection connection, object message) => true; /// public override void Timeout() @@ -227,17 +264,20 @@ namespace CryptoExchange.Net.Sockets else Result = new CallResult(default, null, default); - ContinueAwaiter?.Set(); _event.Set(); + OnComplete?.Invoke(); } /// public override void Fail(Error error) { + if (Completed) + return; + Result = new CallResult(error); Completed = true; - ContinueAwaiter?.Set(); _event.Set(); + OnComplete?.Invoke(); } } } diff --git a/CryptoExchange.Net/Sockets/WebsocketFactory.cs b/CryptoExchange.Net/Sockets/WebsocketFactory.cs deleted file mode 100644 index 286d37d..0000000 --- a/CryptoExchange.Net/Sockets/WebsocketFactory.cs +++ /dev/null @@ -1,18 +0,0 @@ -using CryptoExchange.Net.Interfaces; -using CryptoExchange.Net.Objects.Sockets; -using Microsoft.Extensions.Logging; - -namespace CryptoExchange.Net.Sockets -{ - /// - /// Default websocket factory implementation - /// - public class WebsocketFactory : IWebsocketFactory - { - /// - public IWebsocket CreateWebsocket(ILogger logger, WebSocketParameters parameters) - { - return new CryptoExchangeWebSocketClient(logger, parameters); - } - } -} diff --git a/CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs b/CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs index 59600a1..63dd22b 100644 --- a/CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs +++ b/CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.Globalization; using System.Linq; using System.Text.Json; -using System.Text.Json.Nodes; using System.Text.Json.Serialization; using CryptoExchange.Net.Converters; using CryptoExchange.Net.Converters.SystemTextJson; diff --git a/CryptoExchange.Net/Testing/Implementations/TestRequest.cs b/CryptoExchange.Net/Testing/Implementations/TestRequest.cs index ce5f07e..1cb84ec 100644 --- a/CryptoExchange.Net/Testing/Implementations/TestRequest.cs +++ b/CryptoExchange.Net/Testing/Implementations/TestRequest.cs @@ -1,7 +1,7 @@ using CryptoExchange.Net.Interfaces; using System; -using System.Collections.Generic; using System.Net.Http; +using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -10,10 +10,10 @@ namespace CryptoExchange.Net.Testing.Implementations { internal class TestRequest : IRequest { - private readonly List> _headers = new(); + private readonly HttpRequestHeaders _headers = new HttpRequestMessage().Headers; private readonly TestResponse _response; - public string Accept { set { } } + public MediaTypeWithQualityHeaderValue Accept { set { } } public string? Content { get; private set; } @@ -34,10 +34,10 @@ namespace CryptoExchange.Net.Testing.Implementations public void AddHeader(string key, string value) { - _headers.Add(new KeyValuePair(key, new[] { value })); + _headers.Add(key, value); } - public KeyValuePair[] GetHeaders() => _headers.ToArray(); + public HttpRequestHeaders GetHeaders() => _headers; public Task GetResponseAsync(CancellationToken cancellationToken) => Task.FromResult(_response); diff --git a/CryptoExchange.Net/Testing/Implementations/TestResponse.cs b/CryptoExchange.Net/Testing/Implementations/TestResponse.cs index 1666336..0a6e079 100644 --- a/CryptoExchange.Net/Testing/Implementations/TestResponse.cs +++ b/CryptoExchange.Net/Testing/Implementations/TestResponse.cs @@ -1,8 +1,9 @@ using CryptoExchange.Net.Interfaces; using System; -using System.Collections.Generic; using System.IO; using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; @@ -19,7 +20,7 @@ namespace CryptoExchange.Net.Testing.Implementations public long? ContentLength { get; } - public KeyValuePair[] ResponseHeaders { get; } = new KeyValuePair[0]; + public HttpResponseHeaders ResponseHeaders { get; } = new HttpResponseMessage().Headers; public TestResponse(HttpStatusCode code, Stream response) { diff --git a/CryptoExchange.Net/Testing/Implementations/TestSocket.cs b/CryptoExchange.Net/Testing/Implementations/TestSocket.cs index 5df0dae..35ea438 100644 --- a/CryptoExchange.Net/Testing/Implementations/TestSocket.cs +++ b/CryptoExchange.Net/Testing/Implementations/TestSocket.cs @@ -1,11 +1,11 @@ using System; using System.Net.WebSockets; using System.Text; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Sockets.Default; +using CryptoExchange.Net.Sockets.Default.Interfaces; #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. @@ -39,10 +39,20 @@ namespace CryptoExchange.Net.Testing.Implementations public Func>? GetReconnectionUrl { get; set; } public static int lastId = 0; - public static object lastIdLock = new object(); +#if NET9_0_OR_GREATER + public static readonly Lock lastIdLock = new Lock(); +#else + public static readonly object lastIdLock = new object(); +#endif - public TestSocket(string address) + private bool _newDeserialization; + + public SocketConnection? Connection { get; set; } + + public TestSocket(bool newDeserialization, string address) { + _newDeserialization = newDeserialization; + Uri = new Uri(address); lock (lastIdLock) { @@ -97,15 +107,20 @@ namespace CryptoExchange.Net.Testing.Implementations public void InvokeMessage(string data) { - OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory(Encoding.UTF8.GetBytes(data))).Wait(); + if (!_newDeserialization) + { + OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory(Encoding.UTF8.GetBytes(data))).Wait(); + } + else + { + if (Connection == null) + throw new ArgumentNullException(nameof(Connection)); + + Connection.HandleStreamMessage2(WebSocketMessageType.Text, Encoding.UTF8.GetBytes(data)); + } } - public void InvokeMessage(T data) - { - OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(data)))).Wait(); - } - - public Task ReconnectAsync() => throw new NotImplementedException(); + public Task ReconnectAsync() => Task.CompletedTask; public void Dispose() { } public void UpdateProxy(ApiProxy? proxy) => throw new NotImplementedException(); diff --git a/CryptoExchange.Net/Testing/Implementations/TestWebsocketFactory.cs b/CryptoExchange.Net/Testing/Implementations/TestWebsocketFactory.cs index 3fac7e9..5739721 100644 --- a/CryptoExchange.Net/Testing/Implementations/TestWebsocketFactory.cs +++ b/CryptoExchange.Net/Testing/Implementations/TestWebsocketFactory.cs @@ -1,6 +1,10 @@ -using CryptoExchange.Net.Interfaces; -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Sockets.Default; +using CryptoExchange.Net.Sockets.Default.Interfaces; +using CryptoExchange.Net.Sockets.HighPerf.Interfaces; using Microsoft.Extensions.Logging; +using System; +using System.IO.Pipelines; namespace CryptoExchange.Net.Testing.Implementations { @@ -12,6 +16,13 @@ namespace CryptoExchange.Net.Testing.Implementations _socket = socket; } - public IWebsocket CreateWebsocket(ILogger logger, WebSocketParameters parameters) => _socket; + public IHighPerfWebsocket CreateHighPerfWebsocket(ILogger logger, WebSocketParameters parameters, PipeWriter pipeWriter) + => throw new NotImplementedException(); + + public IWebsocket CreateWebsocket(ILogger logger, SocketConnection connection, WebSocketParameters parameters) + { + _socket.Connection = connection; + return _socket; + } } } diff --git a/CryptoExchange.Net/Testing/RestIntegrationTest.cs b/CryptoExchange.Net/Testing/RestIntegrationTest.cs index 18b359b..69a2db1 100644 --- a/CryptoExchange.Net/Testing/RestIntegrationTest.cs +++ b/CryptoExchange.Net/Testing/RestIntegrationTest.cs @@ -17,8 +17,6 @@ namespace CryptoExchange.Net.Testing /// /// Get a client instance /// - /// - /// public abstract TClient GetClient(ILoggerFactory loggerFactory); /// diff --git a/CryptoExchange.Net/Testing/SocketIntegrationTest.cs b/CryptoExchange.Net/Testing/SocketIntegrationTest.cs index d00ed06..f099bcc 100644 --- a/CryptoExchange.Net/Testing/SocketIntegrationTest.cs +++ b/CryptoExchange.Net/Testing/SocketIntegrationTest.cs @@ -1,5 +1,4 @@ -using CryptoExchange.Net.Interfaces; -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Sockets; using Microsoft.Extensions.Logging; using System; @@ -19,9 +18,7 @@ namespace CryptoExchange.Net.Testing /// /// Get a client instance /// - /// - /// - public abstract TClient GetClient(ILoggerFactory loggerFactory); + public abstract TClient GetClient(ILoggerFactory loggerFactory, bool newDeserialization); /// /// Whether the test should be run. By default integration tests aren't executed, can be set to true to force execution. @@ -37,11 +34,11 @@ namespace CryptoExchange.Net.Testing /// Create a client /// /// - protected TClient CreateClient() + protected TClient CreateClient(bool useNewDeserialization) { var fact = new LoggerFactory(); fact.AddProvider(new TraceLoggerProvider()); - return GetClient(fact); + return GetClient(fact, useNewDeserialization); } /// @@ -61,15 +58,16 @@ namespace CryptoExchange.Net.Testing /// Execute a REST endpoint call and check for any errors or warnings. /// /// Type of the update + /// Whether to use the new deserialization method /// The call expression /// Whether an update is expected /// Whether this is an authenticated request - public async Task RunAndCheckUpdate(Expression>, Task>>> expression, bool expectUpdate, bool authRequest) + public async Task RunAndCheckUpdate(bool useNewDeserialization, Expression>, Task>>> expression, bool expectUpdate, bool authRequest) { if (!ShouldRun()) return; - var client = CreateClient(); + var client = CreateClient(useNewDeserialization); var expressionBody = (MethodCallExpression)expression.Body; if (authRequest && !Authenticated) diff --git a/CryptoExchange.Net/Testing/SocketRequestValidator.cs b/CryptoExchange.Net/Testing/SocketRequestValidator.cs index 24446cd..8787d03 100644 --- a/CryptoExchange.Net/Testing/SocketRequestValidator.cs +++ b/CryptoExchange.Net/Testing/SocketRequestValidator.cs @@ -1,6 +1,5 @@ using CryptoExchange.Net.Clients; using CryptoExchange.Net.Objects; -using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Testing.Comparers; using System; using System.Collections.Generic; diff --git a/CryptoExchange.Net/Testing/SocketSubscriptionValidator.cs b/CryptoExchange.Net/Testing/SocketSubscriptionValidator.cs index f3bdee2..322e743 100644 --- a/CryptoExchange.Net/Testing/SocketSubscriptionValidator.cs +++ b/CryptoExchange.Net/Testing/SocketSubscriptionValidator.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.IO; using System.Text; using System.Text.Json; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -39,6 +40,140 @@ namespace CryptoExchange.Net.Testing _nestedPropertyForCompare = nestedPropertyForCompare; } + /// + /// Validate to subscriptions being established concurrently are indeed handled correctly + /// + /// Type of the subscription update + /// Subscription delegate 1 + /// Subscription delegate 2 + /// Name + public async Task ValidateConcurrentAsync( + Func>, Task>> methodInvoke1, + Func>, Task>> methodInvoke2, + string name) + { + var path = Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName; + FileStream file1; + try + { + file1 = File.OpenRead(Path.Combine(path, _folder, $"{name}.txt")); + } + catch (FileNotFoundException) + { + throw new Exception("Response file not found"); + } + + var buffer1 = new byte[file1.Length]; + await file1.ReadAsync(buffer1, 0, (int)file1.Length).ConfigureAwait(false); + file1.Close(); + + var data1 = Encoding.UTF8.GetString(buffer1); + using var reader1 = new StringReader(data1); + + var socket = TestHelpers.ConfigureSocketClient(_client, _baseAddress); + + string? lastMessage1 = null; + string? lastMessage2 = null; + bool turn = false; + socket.OnMessageSend += (x) => + { + if (turn) + lastMessage1 = x; + else + lastMessage2 = x; + + turn = !turn; + }; + + int updates1 = 0; + int updates2 = 0; + Task> task1; + Task> task2; + // Invoke subscription method + try + { + task1 = methodInvoke1(_client, x => { updates1++; }); + task2 = methodInvoke2(_client, x => { updates2++; }); + } + catch (Exception) + { + throw; + } + + string? message1 = null; + string? message2 = null; + + while (true) + { + var line1 = reader1.ReadLine(); + if (line1 == null) + break; + + if (line1.StartsWith(">")) + { + // Expect a message from client to server + if (line1[1] == '1') + message1 = line1.Substring(3); + else + message2 = line1.Substring(3); + + await Task.Delay(100).ConfigureAwait(false); + } + else if (line1.StartsWith("<")) + { + var line = line1.Substring(3); + var matches = Regex.Matches(line, "(\\|.+\\|)"); + if (matches.Count > 0) + { + var match = matches[0]; + var prevMessage = line1[1] == '1' ? lastMessage1 : lastMessage2; + var json = JsonDocument.Parse(prevMessage!); + var propName = match.Value.Substring(1, match.Value.Length - 2); + var split = propName.Split('.'); + var jsonProp = json.RootElement; + foreach (var x in split) + jsonProp = jsonProp.GetProperty(x); + + var value = jsonProp.ValueKind == JsonValueKind.String ? jsonProp.GetString() : jsonProp.GetInt32().ToString(); + line = line.Replace(match.Value, value); + } + + socket.InvokeMessage(line); + } + else if (line1.StartsWith("=")) + { + var line = line1.Substring(3); + var matches = Regex.Matches(line, "(\\|.+\\|)"); + if (matches.Count > 0) + { + var match = matches[0]; + var prevMessage = line1[1] == '1' ? lastMessage1 : lastMessage2; + var json = JsonDocument.Parse(prevMessage!); + var propName = match.Value.Substring(1, match.Value.Length - 2); + var split = propName.Split('.'); + var jsonProp = json.RootElement; + foreach (var x in split) + jsonProp = jsonProp.GetProperty(x); + + var value = jsonProp.ValueKind == JsonValueKind.String ? jsonProp.GetString() : jsonProp.GetInt32().ToString(); + line = line.Replace(match.Value, value); + } + + socket.InvokeMessage(line); + } + } + + var res = await Task.WhenAll(task1, task2).ConfigureAwait(false); + if (!res[0]) + throw new Exception("Subscribe failed: " + res[0].Error!.ToString()); + if (!res[1]) + throw new Exception("Subscribe failed: " + res[1].Error!.ToString()); + + if (updates1 != 1 || updates2 != 1) + throw new Exception($"Expected 1 update for both subscriptions, instead got {updates1} and {updates2}"); + } + + /// /// Validate a subscription /// @@ -93,10 +228,11 @@ namespace CryptoExchange.Net.Testing }; TUpdate? update = default; + Task> task; // Invoke subscription method try { - var task = methodInvoke(_client, x => { update = x.Data; }); + task = methodInvoke(_client, x => { update = x.Data; }); } catch(Exception) { @@ -194,6 +330,10 @@ namespace CryptoExchange.Net.Testing } } + var res = await task.ConfigureAwait(false); + if (!res) + throw new Exception("Subscribe failed: " + res.Error!.ToString()); + await _client.UnsubscribeAllAsync().ConfigureAwait(false); Trace.Listeners.Remove(listener); } diff --git a/CryptoExchange.Net/Testing/TestHelpers.cs b/CryptoExchange.Net/Testing/TestHelpers.cs index 169d186..797c7be 100644 --- a/CryptoExchange.Net/Testing/TestHelpers.cs +++ b/CryptoExchange.Net/Testing/TestHelpers.cs @@ -63,7 +63,7 @@ namespace CryptoExchange.Net.Testing internal static TestSocket ConfigureSocketClient(T client, string address) where T : BaseSocketClient { - var socket = new TestSocket(address); + var socket = new TestSocket(client.ClientOptions.UseUpdatedDeserialization, address); foreach (var apiClient in client.ApiClients.OfType()) { apiClient.SocketFactory = new TestWebsocketFactory(socket); diff --git a/CryptoExchange.Net/Trackers/CompareValue.cs b/CryptoExchange.Net/Trackers/CompareValue.cs index a6fe033..64531a3 100644 --- a/CryptoExchange.Net/Trackers/CompareValue.cs +++ b/CryptoExchange.Net/Trackers/CompareValue.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace CryptoExchange.Net.Trackers { diff --git a/CryptoExchange.Net/Trackers/Klines/IKlineTracker.cs b/CryptoExchange.Net/Trackers/Klines/IKlineTracker.cs index 11b7afc..c83b54a 100644 --- a/CryptoExchange.Net/Trackers/Klines/IKlineTracker.cs +++ b/CryptoExchange.Net/Trackers/Klines/IKlineTracker.cs @@ -1,7 +1,6 @@ using CryptoExchange.Net.Objects; using CryptoExchange.Net.SharedApis; using System; -using System.Collections.Generic; using System.Threading.Tasks; namespace CryptoExchange.Net.Trackers.Klines diff --git a/CryptoExchange.Net/Trackers/Klines/KlineTracker.cs b/CryptoExchange.Net/Trackers/Klines/KlineTracker.cs index cfc4c33..3c475b5 100644 --- a/CryptoExchange.Net/Trackers/Klines/KlineTracker.cs +++ b/CryptoExchange.Net/Trackers/Klines/KlineTracker.cs @@ -6,8 +6,8 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.Trackers.Klines @@ -31,7 +31,11 @@ namespace CryptoExchange.Net.Trackers.Klines /// /// Lock for accessing _data /// - protected readonly object _lock = new object(); +#if NET9_0_OR_GREATER + private readonly Lock _lock = new Lock(); +#else + private readonly object _lock = new object(); +#endif /// /// The last time the window was applied /// diff --git a/CryptoExchange.Net/Trackers/Klines/KlinesCompare.cs b/CryptoExchange.Net/Trackers/Klines/KlinesCompare.cs index 6e1deea..3d1a2da 100644 --- a/CryptoExchange.Net/Trackers/Klines/KlinesCompare.cs +++ b/CryptoExchange.Net/Trackers/Klines/KlinesCompare.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace CryptoExchange.Net.Trackers.Klines +namespace CryptoExchange.Net.Trackers.Klines { /// /// Klines statistics comparison diff --git a/CryptoExchange.Net/Trackers/Klines/KlinesStats.cs b/CryptoExchange.Net/Trackers/Klines/KlinesStats.cs index 2a832f1..f9c1793 100644 --- a/CryptoExchange.Net/Trackers/Klines/KlinesStats.cs +++ b/CryptoExchange.Net/Trackers/Klines/KlinesStats.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace CryptoExchange.Net.Trackers.Klines { diff --git a/CryptoExchange.Net/Trackers/Trades/ITradeTracker.cs b/CryptoExchange.Net/Trackers/Trades/ITradeTracker.cs index 4f08aea..2f81cd3 100644 --- a/CryptoExchange.Net/Trackers/Trades/ITradeTracker.cs +++ b/CryptoExchange.Net/Trackers/Trades/ITradeTracker.cs @@ -1,7 +1,6 @@ using CryptoExchange.Net.Objects; using CryptoExchange.Net.SharedApis; using System; -using System.Collections.Generic; using System.Threading.Tasks; namespace CryptoExchange.Net.Trackers.Trades diff --git a/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs b/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs index 460f779..76c1732 100644 --- a/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs +++ b/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs @@ -6,8 +6,8 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.Trackers.Trades @@ -42,7 +42,11 @@ namespace CryptoExchange.Net.Trackers.Trades /// /// Lock for accessing _data /// - protected readonly object _lock = new object(); +#if NET9_0_OR_GREATER + private readonly Lock _lock = new Lock(); +#else + private readonly object _lock = new object(); +#endif /// /// Whether the snapshot has been set /// diff --git a/CryptoExchange.Net/Trackers/Trades/TradesCompare.cs b/CryptoExchange.Net/Trackers/Trades/TradesCompare.cs index 1d02593..3264f8e 100644 --- a/CryptoExchange.Net/Trackers/Trades/TradesCompare.cs +++ b/CryptoExchange.Net/Trackers/Trades/TradesCompare.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace CryptoExchange.Net.Trackers.Trades +namespace CryptoExchange.Net.Trackers.Trades { /// /// Trades statistics comparison diff --git a/CryptoExchange.Net/Trackers/Trades/TradesStats.cs b/CryptoExchange.Net/Trackers/Trades/TradesStats.cs index 74107ca..a0d2edb 100644 --- a/CryptoExchange.Net/Trackers/Trades/TradesStats.cs +++ b/CryptoExchange.Net/Trackers/Trades/TradesStats.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace CryptoExchange.Net.Trackers.Trades {