mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2025-12-31 11:07:04 +00:00
Compare commits
5 Commits
f125bc88b0
...
451d38d5e7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
451d38d5e7 | ||
|
|
e11e437bbb | ||
|
|
b8a1ad798d | ||
|
|
4a851c44f2 | ||
|
|
d079796020 |
2
.github/workflows/dotnet.yml
vendored
2
.github/workflows/dotnet.yml
vendored
@ -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
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks>
|
||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<PackageId>CryptoExchange.Net.Protobuf</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>Protobuf support for CryptoExchange.Net</Description>
|
||||
<PackageVersion>9.13.0</PackageVersion>
|
||||
<AssemblyVersion>9.13.0</AssemblyVersion>
|
||||
<FileVersion>9.13.0</FileVersion>
|
||||
<PackageVersion>10.0.1</PackageVersion>
|
||||
<AssemblyVersion>10.0.1</AssemblyVersion>
|
||||
<FileVersion>10.0.1</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
@ -41,7 +41,7 @@
|
||||
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CryptoExchange.Net" Version="9.13.0" />
|
||||
<PackageReference Include="CryptoExchange.Net" Version="10.0.0" />
|
||||
<PackageReference Include="protobuf-net" Version="3.2.56" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@ -5,6 +5,12 @@
|
||||
Protobuf support for CryptoExchange.Net.
|
||||
|
||||
## Release notes
|
||||
* Version 10.0.1 - 16 Dec 2025
|
||||
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 10.0.0 - 16 Dec 2025
|
||||
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.13.0 - 10 Nov 2025
|
||||
* Updated CryptoExchange.Net version to 9.13.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -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<TestObjectResult>(
|
||||
System.Net.HttpStatusCode.OK,
|
||||
HttpVersion.Version11,
|
||||
new KeyValuePair<string, string[]>[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<string, string[]>[0],
|
||||
new HttpRequestMessage().Headers,
|
||||
ResultDataSource.Server,
|
||||
new TestObjectResult(),
|
||||
null);
|
||||
@ -146,7 +143,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result = new WebCallResult<TestObjectResult>(
|
||||
System.Net.HttpStatusCode.OK,
|
||||
HttpVersion.Version11,
|
||||
new KeyValuePair<string, string[]>[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<string, string[]>[0],
|
||||
new HttpRequestMessage().Headers,
|
||||
ResultDataSource.Server,
|
||||
new TestObjectResult(),
|
||||
null);
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0"></PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1"></PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="NUnit" Version="4.4.0"></PackageReference>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="5.2.0"></PackageReference>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="6.0.0"></PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<string, string> 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<string, string> result = null;
|
||||
|
||||
client.SubClient.ConnectSocketSub(sub);
|
||||
// client.SubClient.ConnectSocketSub(sub);
|
||||
|
||||
var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
||||
{
|
||||
result = messageEvent.Data;
|
||||
rstEvent.Set();
|
||||
});
|
||||
sub.AddSubscription(subObj);
|
||||
// var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (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<Dictionary<string, string>>(Mock.Of<ILogger>(), (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<Dictionary<string, string>>(Mock.Of<ILogger>(), (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<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
||||
var ups = new UpdateSubscription(sub, subscription);
|
||||
sub.AddSubscription(subscription);
|
||||
// var subscription = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (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<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
||||
var subscription2 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (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<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
||||
// var subscription2 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (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);
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<SubResponse>
|
||||
{
|
||||
public TestChannelQuery(string channel, string request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
|
||||
{
|
||||
MessageMatcher = MessageMatcher.Create<SubResponse>(request + "-" + channel, HandleMessage);
|
||||
}
|
||||
|
||||
public CallResult<SubResponse> HandleMessage(SocketConnection connection, DataEvent<SubResponse> message)
|
||||
{
|
||||
if (!message.Data.Status.Equals("confirmed", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new CallResult<SubResponse>(new ServerError(ErrorInfo.Unknown with { Message = message.Data.Status }));
|
||||
}
|
||||
|
||||
return message.ToCallResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<object>
|
||||
{
|
||||
public TestQuery(string identifier, object request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
|
||||
{
|
||||
MessageMatcher = MessageMatcher.Create<object>(identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<T> : Subscription<object, object>
|
||||
{
|
||||
private readonly Action<DataEvent<T>> _handler;
|
||||
|
||||
public TestSubscription(ILogger logger, Action<DataEvent<T>> handler) : base(logger, false)
|
||||
{
|
||||
_handler = handler;
|
||||
|
||||
MessageMatcher = MessageMatcher.Create<T>("update-topic", DoHandleMessage);
|
||||
}
|
||||
|
||||
public CallResult DoHandleMessage(SocketConnection connection, DataEvent<T> message)
|
||||
{
|
||||
_handler.Invoke(message);
|
||||
return new CallResult(null);
|
||||
}
|
||||
|
||||
protected override Query GetSubQuery(SocketConnection connection) => new TestQuery("sub", new object(), false, 1);
|
||||
protected override Query GetUnsubQuery(SocketConnection connection) => new TestQuery("unsub", new object(), false, 1);
|
||||
}
|
||||
}
|
||||
@ -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<T> : Subscription<SubResponse, UnsubResponse>
|
||||
{
|
||||
private readonly Action<DataEvent<T>> _handler;
|
||||
private readonly string _channel;
|
||||
|
||||
public TestSubscriptionWithResponseCheck(string channel, Action<DataEvent<T>> handler) : base(Mock.Of<ILogger>(), false)
|
||||
{
|
||||
MessageMatcher = MessageMatcher.Create<T>(channel, DoHandleMessage);
|
||||
_handler = handler;
|
||||
_channel = channel;
|
||||
}
|
||||
|
||||
public CallResult DoHandleMessage(SocketConnection connection, DataEvent<T> message)
|
||||
{
|
||||
_handler.Invoke(message);
|
||||
return new CallResult(null);
|
||||
}
|
||||
|
||||
protected override Query GetSubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "subscribe", false, 1);
|
||||
protected override Query GetUnsubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "unsubscribe", false, 1);
|
||||
}
|
||||
}
|
||||
@ -1,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<TestEnvironment> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<CancellationToken>())).Returns(Task.FromResult((Stream)responseStream));
|
||||
|
||||
var headers = new Dictionary<string, string[]>();
|
||||
var headers = new HttpRequestMessage().Headers;
|
||||
var request = new Mock<IRequest>();
|
||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
||||
request.Setup(c => c.SetContent(It.IsAny<string>(), It.IsAny<string>())).Callback(new Action<string, string>((content, type) => { request.Setup(r => r.Content).Returns(content); }));
|
||||
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new string[] { val }));
|
||||
request.Setup(c => c.GetHeaders()).Returns(() => headers.ToArray());
|
||||
request.Setup(c => c.GetHeaders()).Returns(() => headers);
|
||||
|
||||
var factory = Mock.Get(Api1.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
@ -86,7 +87,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
|
||||
var request = new Mock<IRequest>();
|
||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
||||
request.Setup(c => c.GetHeaders()).Returns(new KeyValuePair<string, string[]>[0]);
|
||||
request.Setup(c => c.GetHeaders()).Returns(new HttpRequestMessage().Headers);
|
||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).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<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
||||
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(new KeyValuePair<string, string[]>(key, new string[] { val })));
|
||||
request.Setup(c => c.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<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
@ -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<IRequestFactory>().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<IRequestFactory>().Object;
|
||||
@ -194,13 +199,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
|
||||
}
|
||||
|
||||
protected override Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor, Exception exception)
|
||||
{
|
||||
var errorData = accessor.Deserialize<TestError>();
|
||||
|
||||
return new ServerError(errorData.Data.ErrorCode, GetErrorInfo(errorData.Data.ErrorCode, errorData.Data.ErrorMessage));
|
||||
}
|
||||
|
||||
public override TimeSpan? GetTimeOffset()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
internal class TestRestMessageHandler : JsonRestMessageHandler
|
||||
{
|
||||
private ErrorMapping _errorMapping = new ErrorMapping([]);
|
||||
public override JsonSerializerOptions Options => new JsonSerializerOptions();
|
||||
|
||||
public override ValueTask<Error> ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream)
|
||||
{
|
||||
var errorData = JsonSerializer.Deserialize<TestError>(responseStream);
|
||||
|
||||
return new ValueTask<Error>(new ServerError(errorData.ErrorCode, _errorMapping.GetErrorInfo(errorData.ErrorCode.ToString(), errorData.ErrorMessage)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,132 +0,0 @@
|
||||
//using System;
|
||||
//using System.IO;
|
||||
//using System.Net.WebSockets;
|
||||
//using System.Security.Authentication;
|
||||
//using System.Text;
|
||||
//using System.Threading.Tasks;
|
||||
//using CryptoExchange.Net.Interfaces;
|
||||
//using CryptoExchange.Net.Objects;
|
||||
|
||||
//namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
//{
|
||||
// public class TestSocket: IWebsocket
|
||||
// {
|
||||
// public bool CanConnect { get; set; }
|
||||
// public bool Connected { get; set; }
|
||||
|
||||
// public event Func<Task> OnClose;
|
||||
//#pragma warning disable 0067
|
||||
// public event Func<Task> OnReconnected;
|
||||
// public event Func<Task> OnReconnecting;
|
||||
// public event Func<int, Task> OnRequestRateLimited;
|
||||
//#pragma warning restore 0067
|
||||
// public event Func<int, Task> OnRequestSent;
|
||||
// public event Func<WebSocketMessageType, ReadOnlyMemory<byte>, Task> OnStreamMessage;
|
||||
// public event Func<Exception, Task> OnError;
|
||||
// public event Func<Task> OnOpen;
|
||||
// public Func<Task<Uri>> GetReconnectionUrl { get; set; }
|
||||
|
||||
// public int Id { get; }
|
||||
// public bool ShouldReconnect { get; set; }
|
||||
// public TimeSpan Timeout { get; set; }
|
||||
// public Func<string, string> DataInterpreterString { get; set; }
|
||||
// public Func<byte[], string> DataInterpreterBytes { get; set; }
|
||||
// public DateTime? DisconnectTime { get; set; }
|
||||
// public string Url { get; }
|
||||
// public bool IsClosed => !Connected;
|
||||
// public bool IsOpen => Connected;
|
||||
// public bool PingConnection { get; set; }
|
||||
// public TimeSpan PingInterval { get; set; }
|
||||
// public SslProtocols SSLProtocols { get; set; }
|
||||
// public Encoding Encoding { get; set; }
|
||||
|
||||
// public int ConnectCalls { get; private set; }
|
||||
// public bool Reconnecting { get; set; }
|
||||
// public string Origin { get; set; }
|
||||
// public int? RatelimitPerSecond { get; set; }
|
||||
|
||||
// public double IncomingKbps => throw new NotImplementedException();
|
||||
|
||||
// public Uri Uri => new Uri("");
|
||||
|
||||
// public TimeSpan KeepAliveInterval { get; set; }
|
||||
|
||||
// public static int lastId = 0;
|
||||
// public static object lastIdLock = new object();
|
||||
|
||||
// public TestSocket()
|
||||
// {
|
||||
// lock (lastIdLock)
|
||||
// {
|
||||
// Id = lastId + 1;
|
||||
// lastId++;
|
||||
// }
|
||||
// }
|
||||
|
||||
// public Task<CallResult> ConnectAsync()
|
||||
// {
|
||||
// Connected = CanConnect;
|
||||
// ConnectCalls++;
|
||||
// if (CanConnect)
|
||||
// InvokeOpen();
|
||||
// return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError()));
|
||||
// }
|
||||
|
||||
// public bool Send(int requestId, string data, int weight)
|
||||
// {
|
||||
// if(!Connected)
|
||||
// throw new Exception("Socket not connected");
|
||||
// OnRequestSent?.Invoke(requestId);
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// public void Reset()
|
||||
// {
|
||||
// }
|
||||
|
||||
// public Task CloseAsync()
|
||||
// {
|
||||
// Connected = false;
|
||||
// DisconnectTime = DateTime.UtcNow;
|
||||
// OnClose?.Invoke();
|
||||
// return Task.FromResult(0);
|
||||
// }
|
||||
|
||||
// public void SetProxy(string host, int port)
|
||||
// {
|
||||
// throw new NotImplementedException();
|
||||
// }
|
||||
// public void Dispose()
|
||||
// {
|
||||
// }
|
||||
|
||||
// public void InvokeClose()
|
||||
// {
|
||||
// Connected = false;
|
||||
// DisconnectTime = DateTime.UtcNow;
|
||||
// Reconnecting = true;
|
||||
// OnClose?.Invoke();
|
||||
// }
|
||||
|
||||
// public void InvokeOpen()
|
||||
// {
|
||||
// OnOpen?.Invoke();
|
||||
// }
|
||||
|
||||
// public void InvokeMessage(string data)
|
||||
// {
|
||||
// OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(data))).Wait();
|
||||
// }
|
||||
|
||||
// public void SetProxy(ApiProxy proxy)
|
||||
// {
|
||||
// throw new NotImplementedException();
|
||||
// }
|
||||
|
||||
// public void InvokeError(Exception error)
|
||||
// {
|
||||
// OnError?.Invoke(error);
|
||||
// }
|
||||
// public Task ReconnectAsync() => Task.CompletedTask;
|
||||
// }
|
||||
//}
|
||||
@ -1,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; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of KucoinSocketClient
|
||||
/// </summary>
|
||||
/// <param name="optionsFunc">Configure the options to use for this client</param>
|
||||
public TestSocketClient(Action<TestSocketOptions> optionsDelegate = null)
|
||||
: this(Options.Create(ApplyOptionsDelegate(optionsDelegate)), null)
|
||||
{
|
||||
}
|
||||
|
||||
public TestSocketClient(IOptions<TestSocketOptions> options, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
|
||||
{
|
||||
Initialize(options.Value);
|
||||
|
||||
SubClient = AddApiClient(new TestSubSocketClient(options.Value, options.Value.SubOptions));
|
||||
SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object;
|
||||
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com"));
|
||||
}
|
||||
|
||||
public TestSocket CreateSocket()
|
||||
{
|
||||
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com"));
|
||||
return (TestSocket)SubClient.CreateSocketInternal("https://localhost:123/");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class TestEnvironment : TradeEnvironment
|
||||
{
|
||||
public string TestAddress { get; }
|
||||
|
||||
public TestEnvironment(string name, string url) : base(name)
|
||||
{
|
||||
TestAddress = url;
|
||||
}
|
||||
}
|
||||
|
||||
public class TestSocketOptions: SocketExchangeOptions<TestEnvironment>
|
||||
{
|
||||
public static TestSocketOptions Default = new TestSocketOptions
|
||||
{
|
||||
Environment = new TestEnvironment("Live", "https://test.test")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public TestSocketOptions()
|
||||
{
|
||||
Default?.Set(this);
|
||||
}
|
||||
|
||||
public SocketApiOptions SubOptions { get; set; } = new SocketApiOptions();
|
||||
|
||||
internal TestSocketOptions Set(TestSocketOptions targetOptions)
|
||||
{
|
||||
targetOptions = base.Set<TestSocketOptions>(targetOptions);
|
||||
targetOptions.SubOptions = SubOptions.Set(targetOptions.SubOptions);
|
||||
return targetOptions;
|
||||
}
|
||||
}
|
||||
|
||||
public class TestSubSocketClient : SocketApiClient
|
||||
{
|
||||
private MessagePath _channelPath = MessagePath.Get().Property("channel");
|
||||
private MessagePath _actionPath = MessagePath.Get().Property("action");
|
||||
private MessagePath _topicPath = MessagePath.Get().Property("topic");
|
||||
|
||||
public Subscription TestSubscription { get; private set; } = null;
|
||||
|
||||
public TestSubSocketClient(TestSocketOptions options, SocketApiOptions apiOptions) : base(new TraceLogger(), options.Environment.TestAddress, options, apiOptions)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected internal override IByteMessageAccessor CreateAccessor(WebSocketMessageType type) => new SystemTextJsonByteMessageAccessor(new System.Text.Json.JsonSerializerOptions());
|
||||
protected internal override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||
|
||||
internal IWebsocket CreateSocketInternal(string address)
|
||||
{
|
||||
return CreateSocket(address);
|
||||
}
|
||||
|
||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||
=> new TestAuthProvider(credentials);
|
||||
|
||||
public CallResult ConnectSocketSub(SocketConnection sub)
|
||||
{
|
||||
return ConnectSocketAsync(sub, default).Result;
|
||||
}
|
||||
|
||||
public override string GetListenerIdentifier(IMessageAccessor message)
|
||||
{
|
||||
if (!message.IsValid)
|
||||
{
|
||||
return "topic";
|
||||
}
|
||||
|
||||
var id = message.GetValue<string>(_channelPath);
|
||||
id ??= message.GetValue<string>(_topicPath);
|
||||
|
||||
return message.GetValue<string>(_actionPath) + "-" + id;
|
||||
}
|
||||
|
||||
public Task<CallResult<UpdateSubscription>> SubscribeToSomethingAsync(string channel, Action<DataEvent<string>> onUpdate, CancellationToken ct)
|
||||
{
|
||||
TestSubscription = new TestSubscriptionWithResponseCheck<string>(channel, onUpdate);
|
||||
return SubscribeAsync(TestSubscription, ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,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
|
||||
{
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create API credentials using an API key and secret generated by the server
|
||||
/// </summary>
|
||||
public static ApiCredentials HmacCredentials(string apiKey, string apiSecret, string? pass)
|
||||
{
|
||||
return new ApiCredentials(apiKey, apiSecret, pass, ApiCredentialsType.Hmac);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create API credentials using an API key and an RSA private key in PEM format
|
||||
/// </summary>
|
||||
public static ApiCredentials RsaPemCredentials(string apiKey, string privateKey)
|
||||
{
|
||||
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.RsaPem);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create API credentials using an API key and an RSA private key in XML format
|
||||
/// </summary>
|
||||
public static ApiCredentials RsaXmlCredentials(string apiKey, string privateKey)
|
||||
{
|
||||
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.RsaXml);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create API credentials using an API key and an Ed25519 private key
|
||||
/// </summary>
|
||||
public static ApiCredentials Ed25519Credentials(string apiKey, string privateKey)
|
||||
{
|
||||
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.Ed25519);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load a key from a file
|
||||
/// </summary>
|
||||
public static string ReadFromFile(string path)
|
||||
{
|
||||
using var fileStream = File.OpenRead(path);
|
||||
using var streamReader = new StreamReader(fileStream);
|
||||
return streamReader.ReadToEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the credentials
|
||||
/// </summary>
|
||||
|
||||
@ -16,6 +16,10 @@
|
||||
/// <summary>
|
||||
/// Rsa keys credentials in pem/base64 format. Only available for .NetStandard 2.1 and up, use xml format for lower.
|
||||
/// </summary>
|
||||
RsaPem
|
||||
RsaPem,
|
||||
/// <summary>
|
||||
/// Ed25519 keys credentials
|
||||
/// </summary>
|
||||
Ed25519
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
/// <summary>
|
||||
/// The supported credential types
|
||||
/// </summary>
|
||||
public abstract ApiCredentialsType[] SupportedCredentialTypes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Provided credentials
|
||||
/// </summary>
|
||||
@ -28,6 +36,13 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// </summary>
|
||||
protected byte[] _sBytes;
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
/// <summary>
|
||||
/// The Ed25519 private key
|
||||
/// </summary>
|
||||
protected Key? Ed25519Key;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Get the API key of the current credentials
|
||||
/// </summary>
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ed25519 sign the data
|
||||
/// </summary>
|
||||
public string SignEd25519(string data, SignOutputType? outputType = null)
|
||||
=> SignEd25519(Encoding.ASCII.GetBytes(data), outputType);
|
||||
|
||||
/// <summary>
|
||||
/// Ed25519 sign the data
|
||||
/// </summary>
|
||||
public string SignEd25519(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
if (Ed25519Key == null)
|
||||
{
|
||||
var key = _credentials.Secret!
|
||||
.Replace("\n", "")
|
||||
.Replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
.Replace("-----END PRIVATE KEY-----", "")
|
||||
.Trim();
|
||||
var keyBytes = Convert.FromBase64String(key);
|
||||
Ed25519Key = Key.Import(SignatureAlgorithm.Ed25519, keyBytes, KeyBlobFormat.PkixPrivateKey);
|
||||
}
|
||||
|
||||
var resultBytes = SignatureAlgorithm.Ed25519.Sign(Ed25519Key, data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
#else
|
||||
throw new InvalidOperationException();
|
||||
#endif
|
||||
}
|
||||
|
||||
private RSA CreateRSA()
|
||||
{
|
||||
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);
|
||||
|
||||
@ -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<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
|
||||
#if NET9_0_OR_GREATER
|
||||
private readonly Lock _lock = new Lock();
|
||||
#else
|
||||
private readonly object _lock = new object();
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Add a new cache entry. Will override an existing entry if it already exists
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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<SocketApiClient>().Sum(s => s.CurrentSubscriptions);
|
||||
/// <inheritdoc />
|
||||
public double IncomingKbps => ApiClients.OfType<SocketApiClient>().Sum(s => s.IncomingKbps);
|
||||
|
||||
/// <inheritdoc />
|
||||
public new SocketExchangeOptions ClientOptions => (SocketExchangeOptions)base.ClientOptions;
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Interfaces.Clients;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Clients
|
||||
|
||||
@ -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
|
||||
/// </summary>
|
||||
private readonly static MemoryCache _cache = new MemoryCache();
|
||||
|
||||
/// <summary>
|
||||
/// The message handler
|
||||
/// </summary>
|
||||
protected abstract IRestMessageHandler MessageHandler { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
@ -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<T>(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<T>(prepareResult.Error!);
|
||||
var error = await CheckTimeSync(requestId, definition).ConfigureAwait(false);
|
||||
if (error != null)
|
||||
return new WebCallResult<T>(error);
|
||||
|
||||
error = await RateLimitAsync(
|
||||
baseAddress,
|
||||
requestId,
|
||||
definition,
|
||||
weight ?? definition.Weight,
|
||||
cancellationToken,
|
||||
weightSingleLimiter,
|
||||
rateLimitKeySuffix).ConfigureAwait(false);
|
||||
if (error != null)
|
||||
return new WebCallResult<T>(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<T>(definition, request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var result = await GetResponseAsync2<T>(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<Error?> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepare before sending a request. Sync time between client and server and check rate limits
|
||||
/// Check rate limits for the request
|
||||
/// </summary>
|
||||
/// <param name="requestId">Request id</param>
|
||||
/// <param name="baseAddress">Host and schema</param>
|
||||
/// <param name="definition">Request definition</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <param name="additionalHeaders">Additional headers for this request</param>
|
||||
/// <param name="weight">Override the request weight for this request</param>
|
||||
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
|
||||
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
protected virtual async Task<CallResult> PrepareAsync(
|
||||
protected virtual async ValueTask<Error?> RateLimitAsync(
|
||||
string host,
|
||||
int requestId,
|
||||
string baseAddress,
|
||||
RequestDefinition definition,
|
||||
int weight,
|
||||
CancellationToken cancellationToken,
|
||||
Dictionary<string, string>? 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<IRequest>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -367,9 +388,9 @@ namespace CryptoExchange.Net.Clients
|
||||
var requestConfiguration = new RestRequestConfiguration(
|
||||
definition,
|
||||
baseAddress,
|
||||
uriParameters == null ? new Dictionary<string, object>() : CreateParameterDictionary(uriParameters),
|
||||
bodyParameters == null ? new Dictionary<string, object>() : CreateParameterDictionary(bodyParameters),
|
||||
new Dictionary<string, string>(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<string, string>();
|
||||
if (!requestConfiguration.Headers.ContainsKey(header.Key))
|
||||
request.AddHeader(header.Key, header.Value);
|
||||
}
|
||||
@ -429,7 +454,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="gate">The ratelimit gate used</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(
|
||||
protected virtual async Task<WebCallResult<T>> GetResponseAsync2<T>(
|
||||
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<T>(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<T>(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<T>(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<T>(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<T>(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<T>(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<T>(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<T>();
|
||||
return new WebCallResult<T>(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<T>(responseStream, cancellationToken).ConfigureAwait(false);
|
||||
if (deserializeError != null)
|
||||
return new WebCallResult<T>(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<T>(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<T>(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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <param name="requestDefinition">Request definition</param>
|
||||
/// <param name="accessor">Data accessor</param>
|
||||
/// <param name="responseHeaders">The response headers</param>
|
||||
/// <returns>Null if not an error, Error otherwise</returns>
|
||||
protected virtual Error? TryParseError(RequestDefinition requestDefinition, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor) => null;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <param name="callResult">The result of the call</param>
|
||||
/// <param name="tries">The current try number</param>
|
||||
/// <returns>True if call should retry, false if the call should return</returns>
|
||||
protected virtual async Task<bool> ShouldRetryRequestAsync<T>(IRateLimitGate? gate, WebCallResult<T> callResult, int tries)
|
||||
protected virtual async ValueTask<bool> ShouldRetryRequestAsync<T>(IRateLimitGate? gate, WebCallResult<T> callResult, int tries)
|
||||
{
|
||||
if (tries >= 2)
|
||||
// Only retry once
|
||||
@ -632,43 +679,6 @@ namespace CryptoExchange.Net.Clients
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse an error response from the server. Only used when server returns a status other than Success(200) or ratelimit error (429 or 418)
|
||||
/// </summary>
|
||||
/// <param name="httpStatusCode">The response status code</param>
|
||||
/// <param name="responseHeaders">The response headers</param>
|
||||
/// <param name="accessor">Data accessor</param>
|
||||
/// <param name="exception">Exception</param>
|
||||
/// <returns></returns>
|
||||
protected virtual Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor, Exception? exception)
|
||||
{
|
||||
return new ServerError(ErrorInfo.Unknown, exception);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a rate limit error response from the server. Only used when server returns http status 429 or 418
|
||||
/// </summary>
|
||||
/// <param name="httpStatusCode">The response status code</param>
|
||||
/// <param name="responseHeaders">The response headers</param>
|
||||
/// <param name="accessor">Data accessor</param>
|
||||
/// <returns></returns>
|
||||
protected virtual ServerRateLimitError ParseRateLimitResponse(int httpStatusCode, KeyValuePair<string, string[]>[] 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the parameter IDictionary
|
||||
/// </summary>
|
||||
@ -696,18 +706,18 @@ namespace CryptoExchange.Net.Clients
|
||||
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout, ClientOptions.HttpKeepAliveInterval);
|
||||
}
|
||||
|
||||
internal async Task<WebCallResult<bool>> SyncTimeAsync()
|
||||
internal async ValueTask<Error?> SyncTimeAsync()
|
||||
{
|
||||
var timeSyncParams = GetTimeSyncInfo();
|
||||
if (timeSyncParams == null)
|
||||
return new WebCallResult<bool>(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<bool>(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<bool>(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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
/// <inheritdoc/>
|
||||
public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory();
|
||||
/// <inheritdoc/>
|
||||
public IHighPerfConnectionFactory? HighPerfConnectionFactory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of socket connections currently connecting/connected
|
||||
/// </summary>
|
||||
protected internal ConcurrentDictionary<int, SocketConnection> socketConnections = new();
|
||||
protected internal ConcurrentDictionary<int, SocketConnection> _socketConnections = new();
|
||||
|
||||
/// <summary>
|
||||
/// List of HighPerf socket connections currently connecting/connected
|
||||
/// </summary>
|
||||
protected internal ConcurrentDictionary<int, HighPerfSocketConnection> _highPerfSocketConnections = new();
|
||||
|
||||
/// <summary>
|
||||
/// Semaphore used while creating sockets
|
||||
@ -72,7 +86,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// Periodic task registrations
|
||||
/// </summary>
|
||||
protected List<PeriodicTaskRegistration> PeriodicTaskRegistrations { get; set; } = new List<PeriodicTaskRegistration>();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// List of address to keep an alive connection to
|
||||
/// </summary>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CurrentConnections => socketConnections.Count;
|
||||
public int CurrentConnections => _socketConnections.Count;
|
||||
|
||||
/// <inheritdoc />
|
||||
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
|
||||
/// <inheritdoc />
|
||||
public new SocketApiOptions ApiOptions => (SocketApiOptions)base.ApiOptions;
|
||||
|
||||
/// <summary>
|
||||
/// The max number of individual subscriptions on a single connection
|
||||
/// </summary>
|
||||
public int? MaxIndividualSubscriptionsPerConnection { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
@ -169,7 +188,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="interval"></param>
|
||||
/// <param name="queryDelegate"></param>
|
||||
/// <param name="callback"></param>
|
||||
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback)
|
||||
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<ISocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback)
|
||||
{
|
||||
PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration
|
||||
{
|
||||
@ -209,6 +228,9 @@ namespace CryptoExchange.Net.Clients
|
||||
return new CallResult<UpdateSubscription>(new NoApiCredentialsError());
|
||||
}
|
||||
|
||||
if (subscription.IndividualSubscriptionCount > MaxIndividualSubscriptionsPerConnection)
|
||||
return new CallResult<UpdateSubscription>(ArgumentError.Invalid("subscriptions", $"Max number of subscriptions in a single call is {MaxIndividualSubscriptionsPerConnection}"));
|
||||
|
||||
SocketConnection socketConnection;
|
||||
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<UpdateSubscription>(null);
|
||||
|
||||
@ -269,16 +291,33 @@ namespace CryptoExchange.Net.Clients
|
||||
return new CallResult<UpdateSubscription>(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<UpdateSubscription>(subResult.Error!);
|
||||
}
|
||||
}
|
||||
|
||||
subscription.HandleSubQueryResponse(subQuery.Response!);
|
||||
}
|
||||
else
|
||||
{
|
||||
HandleSubscriptionComplete(true, null);
|
||||
}
|
||||
|
||||
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
|
||||
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect to an url and listen for data
|
||||
/// </summary>
|
||||
/// <param name="url">The URL to connect to</param>
|
||||
/// <param name="subscription">The subscription</param>
|
||||
/// <param name="connectionFactory">The factory for creating a socket connection</param>
|
||||
/// <param name="ct">Cancellation token for closing this subscription</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<HighPerfUpdateSubscription>> SubscribeHighPerfAsync<TUpdateType>(
|
||||
string url,
|
||||
HighPerfSubscription<TUpdateType> subscription,
|
||||
IHighPerfConnectionFactory connectionFactory,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (_disposing)
|
||||
return new CallResult<HighPerfUpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
|
||||
|
||||
HighPerfSocketConnection<TUpdateType> socketConnection;
|
||||
var released = false;
|
||||
// Wait for a semaphore here, so we only connect 1 socket at a time.
|
||||
// This is necessary for being able to see if connections can be combined
|
||||
try
|
||||
{
|
||||
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException tce)
|
||||
{
|
||||
return new CallResult<HighPerfUpdateSubscription>(new CancellationRequestedError(tce));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// Get a new or existing socket connection
|
||||
var socketResult = await GetHighPerfSocketConnection<TUpdateType>(url, connectionFactory, ct).ConfigureAwait(false);
|
||||
if (!socketResult)
|
||||
return socketResult.As<HighPerfUpdateSubscription>(null);
|
||||
|
||||
socketConnection = socketResult.Data;
|
||||
|
||||
// Add a subscription on the socket connection
|
||||
var success = socketConnection.AddSubscription(subscription);
|
||||
if (!success)
|
||||
{
|
||||
_logger.FailedToAddSubscriptionRetryOnDifferentConnection(socketConnection.SocketId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
|
||||
{
|
||||
// Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway
|
||||
semaphoreSlim.Release();
|
||||
released = true;
|
||||
}
|
||||
|
||||
var needsConnecting = !socketConnection.Connected;
|
||||
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, false, ct).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return new CallResult<HighPerfUpdateSubscription>(connectResult.Error!);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!released)
|
||||
semaphoreSlim.Release();
|
||||
}
|
||||
|
||||
var subRequest = subscription.CreateSubscriptionQuery(socketConnection);
|
||||
if (subRequest != null)
|
||||
{
|
||||
// Send the request and wait for answer
|
||||
var sendResult = await socketConnection.SendAsync(subRequest).ConfigureAwait(false);
|
||||
if (!sendResult)
|
||||
{
|
||||
await socketConnection.CloseAsync().ConfigureAwait(false);
|
||||
return new CallResult<HighPerfUpdateSubscription>(sendResult.Error!);
|
||||
}
|
||||
}
|
||||
|
||||
subscription.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<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
|
||||
return new CallResult<HighPerfUpdateSubscription>(new HighPerfUpdateSubscription(socketConnection, subscription));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -377,7 +503,7 @@ namespace CryptoExchange.Net.Clients
|
||||
if (ct.IsCancellationRequested)
|
||||
return new CallResult<THandlerResponse>(new CancellationRequestedError());
|
||||
|
||||
return await socketConnection.SendAndWaitQueryAsync(query, null, ct).ConfigureAwait(false);
|
||||
return await socketConnection.SendAndWaitQueryAsync(query, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -387,7 +513,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="authenticated">Whether the socket should authenticated</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult> ConnectIfNeededAsync(SocketConnection socket, bool authenticated, CancellationToken ct)
|
||||
protected virtual async Task<CallResult> 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
|
||||
/// </summary>
|
||||
/// <param name="connection"></param>
|
||||
/// <returns></returns>
|
||||
protected internal virtual Task<Uri?> GetReconnectUriAsync(SocketConnection connection)
|
||||
protected internal virtual Task<Uri?> GetReconnectUriAsync(ISocketConnection connection)
|
||||
{
|
||||
return Task.FromResult<Uri?>(connection.ConnectionUri);
|
||||
}
|
||||
@ -498,10 +627,17 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param>
|
||||
/// <param name="individualSubscriptionCount">The number of individual subscriptions in this subscribe request</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection, CancellationToken ct, string? topic = null)
|
||||
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(
|
||||
string address,
|
||||
bool authenticated,
|
||||
bool dedicatedRequestConnection,
|
||||
CancellationToken ct,
|
||||
string? topic = null,
|
||||
int individualSubscriptionCount = 1)
|
||||
{
|
||||
var socketQuery = socketConnections.Where(s => s.Value.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<SocketConnection>(connection);
|
||||
// If there is a max subscriptions per connection limit also only use existing if the new subscription doesn't go over the limit
|
||||
if (MaxIndividualSubscriptionsPerConnection == null)
|
||||
return new CallResult<SocketConnection>(connection);
|
||||
|
||||
var currentCount = connection.Subscriptions.Sum(x => x.IndividualSubscriptionCount);
|
||||
if (currentCount + individualSubscriptionCount <= MaxIndividualSubscriptionsPerConnection)
|
||||
return new CallResult<SocketConnection>(connection);
|
||||
}
|
||||
}
|
||||
|
||||
if (maxConnectionsReached)
|
||||
return new CallResult<SocketConnection>(new InvalidOperationError("Max amount of socket connections reached"));
|
||||
|
||||
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
||||
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>(socketConnection);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
|
||||
/// </summary>
|
||||
/// <param name="address">The address the socket is for</param>
|
||||
/// <param name="connectionFactory">The factory for creating a socket connection</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<HighPerfSocketConnection<TUpdateType>>> GetHighPerfSocketConnection<TUpdateType>(
|
||||
string address,
|
||||
IHighPerfConnectionFactory connectionFactory,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var connectionAddress = await GetConnectionUrlAsync(address, false).ConfigureAwait(false);
|
||||
if (!connectionAddress)
|
||||
{
|
||||
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error?.ToString());
|
||||
return connectionAddress.As<HighPerfSocketConnection<TUpdateType>>(null);
|
||||
}
|
||||
|
||||
if (connectionAddress.Data != address)
|
||||
_logger.ConnectionAddressSetTo(connectionAddress.Data!);
|
||||
|
||||
// Create new socket connection
|
||||
var socketConnection = connectionFactory.CreateHighPerfConnection<TUpdateType>(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this, address);
|
||||
foreach (var ptg in PeriodicTaskRegistrations)
|
||||
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, (con) => ptg.QueryDelegate(con).Request);
|
||||
|
||||
return new CallResult<HighPerfSocketConnection<TUpdateType>>(socketConnection);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Process an unhandled message
|
||||
/// </summary>
|
||||
@ -622,12 +802,15 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="socketConnection">The socket to connect</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult> ConnectSocketAsync(SocketConnection socketConnection, CancellationToken ct)
|
||||
protected virtual async Task<CallResult> 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
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Create a socket for an address
|
||||
/// </summary>
|
||||
/// <param name="address">The address the socket should connect to</param>
|
||||
/// <returns></returns>
|
||||
protected virtual IWebsocket CreateSocket(string address)
|
||||
{
|
||||
var socket = SocketFactory.CreateWebsocket(_logger, GetWebSocketParameters(address));
|
||||
_logger.SocketCreatedForAddress(socket.Id, address);
|
||||
return socket;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe an update subscription
|
||||
/// </summary>
|
||||
@ -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
|
||||
/// <returns></returns>
|
||||
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<Task>();
|
||||
|
||||
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
|
||||
/// <returns></returns>
|
||||
public virtual async Task ReconnectAsync()
|
||||
{
|
||||
_logger.ReconnectingAllConnections(socketConnections.Count);
|
||||
_logger.ReconnectingAllConnections(_socketConnections.Count);
|
||||
var tasks = new List<Task>();
|
||||
{
|
||||
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
|
||||
/// <returns></returns>
|
||||
public SocketApiClientState GetState(bool includeSubDetails = true)
|
||||
{
|
||||
var connectionStates = new List<SocketConnection.SocketConnectionState>();
|
||||
foreach (var socketIdAndConnection in socketConnections)
|
||||
var connectionStates = new List<SocketConnectionState>();
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -820,7 +996,7 @@ namespace CryptoExchange.Net.Clients
|
||||
int Connections,
|
||||
int Subscriptions,
|
||||
double DownloadSpeed,
|
||||
List<SocketConnection.SocketConnectionState> ConnectionStates)
|
||||
List<SocketConnectionState> ConnectionStates)
|
||||
{
|
||||
/// <summary>
|
||||
/// Print the state of the client
|
||||
@ -868,7 +1044,7 @@ namespace CryptoExchange.Net.Clients
|
||||
_disposing = true;
|
||||
var tasks = new List<Task>();
|
||||
{
|
||||
var socketList = socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected);
|
||||
var socketList = _socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected);
|
||||
if (socketList.Any())
|
||||
_logger.DisposingSocketClient();
|
||||
|
||||
@ -892,10 +1068,16 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <summary>
|
||||
/// Preprocess a stream message
|
||||
/// </summary>
|
||||
/// <param name="connection"></param>
|
||||
/// <param name="type"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
public virtual ReadOnlySpan<byte> PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlySpan<byte> data) => data;
|
||||
/// <summary>
|
||||
/// Preprocess a stream message
|
||||
/// </summary>
|
||||
public virtual ReadOnlyMemory<byte> PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlyMemory<byte> data) => data;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new message converter instance
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public abstract ISocketMessageHandler CreateMessageConverter(WebSocketMessageType messageType);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,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
|
||||
|
||||
@ -0,0 +1,64 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System.IO;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// REST message handler
|
||||
/// </summary>
|
||||
public interface IRestMessageHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// The `accept` HTTP response header for the request
|
||||
/// </summary>
|
||||
MediaTypeWithQualityHeaderValue AcceptHeader { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether a seekable stream is required
|
||||
/// </summary>
|
||||
bool RequiresSeekableStream { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Parse the response when the HTTP response status indicated an error
|
||||
/// </summary>
|
||||
ValueTask<Error> ParseErrorResponse(
|
||||
int httpStatusCode,
|
||||
HttpResponseHeaders responseHeaders,
|
||||
Stream responseStream);
|
||||
|
||||
/// <summary>
|
||||
/// Parse the response when the HTTP response status indicated a rate limit error
|
||||
/// </summary>
|
||||
ValueTask<ServerRateLimitError> ParseErrorRateLimitResponse(
|
||||
int httpStatusCode,
|
||||
HttpResponseHeaders responseHeaders,
|
||||
Stream responseStream);
|
||||
|
||||
/// <summary>
|
||||
/// Check if the response is an error response; if so return the error.<br />
|
||||
/// Note that if the API returns a standard result wrapper, something like this:
|
||||
/// <code>{ "code": 400, "msg": "error", "data": {} }</code>
|
||||
/// then the `CheckDeserializedResponse` method should be used for checking the result
|
||||
/// </summary>
|
||||
ValueTask<Error?> CheckForErrorResponse(
|
||||
RequestDefinition request,
|
||||
HttpResponseHeaders responseHeaders,
|
||||
Stream responseStream);
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize the response stream
|
||||
/// </summary>
|
||||
ValueTask<(T? Result, Error? Error)> TryDeserializeAsync<T>(
|
||||
Stream responseStream,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the resulting T object indicates an error or not
|
||||
/// </summary>
|
||||
Error? CheckDeserializedResponse<T>(HttpResponseHeaders responseHeaders, T result);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Net.WebSockets;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// WebSocket message handler
|
||||
/// </summary>
|
||||
public interface ISocketMessageHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Get an identifier for the message which can be used to determine the type of the message
|
||||
/// </summary>
|
||||
string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType);
|
||||
|
||||
/// <summary>
|
||||
/// Get optional topic filter, for example a symbol name
|
||||
/// </summary>
|
||||
string? GetTopicFilter(object deserializedObject);
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize to the provided type
|
||||
/// </summary>
|
||||
object Deserialize(ReadOnlySpan<byte> data, Type type);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// Message type definition
|
||||
/// </summary>
|
||||
public class MessageTypeDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether to immediately select the definition when it is matched. Can only be used when the evaluator has a single unique field to look for
|
||||
/// </summary>
|
||||
public bool ForceIfFound { get; set; }
|
||||
/// <summary>
|
||||
/// The fields a message needs to contain for this definition
|
||||
/// </summary>
|
||||
public MessageFieldReference[] Fields { get; set; } = [];
|
||||
/// <summary>
|
||||
/// The callback for getting the identifier string
|
||||
/// </summary>
|
||||
public Func<SearchResult, string>? TypeIdentifierCallback { get; set; }
|
||||
/// <summary>
|
||||
/// The static identifier string to return when this evaluator is matched
|
||||
/// </summary>
|
||||
public string? StaticIdentifier { get; set; }
|
||||
|
||||
internal string? GetMessageType(SearchResult result)
|
||||
{
|
||||
if (StaticIdentifier != null)
|
||||
return StaticIdentifier;
|
||||
|
||||
return TypeIdentifierCallback!(result);
|
||||
}
|
||||
|
||||
internal bool Satisfied(SearchResult result)
|
||||
{
|
||||
foreach(var field in Fields)
|
||||
{
|
||||
if (!result.Contains(field))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,152 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// Reference to a message field
|
||||
/// </summary>
|
||||
public abstract class MessageFieldReference
|
||||
{
|
||||
/// <summary>
|
||||
/// The name for this search field
|
||||
/// </summary>
|
||||
public string SearchName { get; set; }
|
||||
/// <summary>
|
||||
/// The depth at which to look for this field
|
||||
/// </summary>
|
||||
public int Depth { get; set; } = 1;
|
||||
/// <summary>
|
||||
/// Callback to check if the field value matches an expected constraint
|
||||
/// </summary>
|
||||
public Func<string?, bool>? Constraint { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the value is one of the string values in the set
|
||||
/// </summary>
|
||||
public MessageFieldReference WithFilterConstraint(HashSet<string?> set)
|
||||
{
|
||||
Constraint = set.Contains;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the value is equal to a string
|
||||
/// </summary>
|
||||
public MessageFieldReference WithEqualConstraint(string compare)
|
||||
{
|
||||
Constraint = x => x != null && x.Equals(compare, StringComparison.Ordinal);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the value is not equal to a string
|
||||
/// </summary>
|
||||
public MessageFieldReference WithNotEqualConstraint(string compare)
|
||||
{
|
||||
Constraint = x => x == null || !x.Equals(compare, StringComparison.Ordinal);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the value is not null
|
||||
/// </summary>
|
||||
public MessageFieldReference WithNotNullConstraint()
|
||||
{
|
||||
Constraint = x => x != null;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the value starts with a certain string
|
||||
/// </summary>
|
||||
public MessageFieldReference WithStartsWithConstraint(string start)
|
||||
{
|
||||
Constraint = x => x != null && x.StartsWith(start, StringComparison.Ordinal);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the value starts with a certain string
|
||||
/// </summary>
|
||||
public MessageFieldReference WithStartsWithConstraints(params string[] startValues)
|
||||
{
|
||||
Constraint = x =>
|
||||
{
|
||||
if (x == null)
|
||||
return false;
|
||||
|
||||
foreach (var item in startValues)
|
||||
{
|
||||
if (x!.StartsWith(item, StringComparison.Ordinal))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the value starts with a certain string
|
||||
/// </summary>
|
||||
public MessageFieldReference WithCustomConstraint(Func<string?, bool> constraint)
|
||||
{
|
||||
Constraint = constraint;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public MessageFieldReference(string searchName)
|
||||
{
|
||||
SearchName = searchName;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference to a property message field
|
||||
/// </summary>
|
||||
public class PropertyFieldReference : MessageFieldReference
|
||||
{
|
||||
/// <summary>
|
||||
/// The property name in the JSON
|
||||
/// </summary>
|
||||
public byte[] PropertyName { get; set; }
|
||||
/// <summary>
|
||||
/// Whether the property value is array values
|
||||
/// </summary>
|
||||
public bool ArrayValues { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public PropertyFieldReference(string propertyName) : base(propertyName)
|
||||
{
|
||||
PropertyName = Encoding.UTF8.GetBytes(propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference to an array message field
|
||||
/// </summary>
|
||||
public class ArrayFieldReference : MessageFieldReference
|
||||
{
|
||||
/// <summary>
|
||||
/// The index in the array
|
||||
/// </summary>
|
||||
public int ArrayIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public ArrayFieldReference(string searchName, int depth, int index) : base(searchName)
|
||||
{
|
||||
Depth = depth;
|
||||
ArrayIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// The results of a search for fields in a JSON message
|
||||
/// </summary>
|
||||
public class SearchResult
|
||||
{
|
||||
private List<SearchResultItem> _items = new List<SearchResultItem>();
|
||||
|
||||
/// <summary>
|
||||
/// Get the value of a field
|
||||
/// </summary>
|
||||
public string? FieldValue(string searchName)
|
||||
{
|
||||
foreach (var item in _items)
|
||||
{
|
||||
if (item.Field.SearchName.Equals(searchName, StringComparison.Ordinal))
|
||||
return item.Value;
|
||||
}
|
||||
|
||||
throw new Exception($"No field value found for {searchName}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The number of found search field values
|
||||
/// </summary>
|
||||
public int Count => _items.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Clear the search result
|
||||
/// </summary>
|
||||
public void Clear() => _items.Clear();
|
||||
|
||||
/// <summary>
|
||||
/// Whether the value for a specific field was found
|
||||
/// </summary>
|
||||
public bool Contains(MessageFieldReference field)
|
||||
{
|
||||
foreach (var item in _items)
|
||||
{
|
||||
if (item.Field == field)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a value to the result
|
||||
/// </summary>
|
||||
public void Write(MessageFieldReference field, string? value) => _items.Add(new SearchResultItem
|
||||
{
|
||||
Field = field,
|
||||
Value = value
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// Search result value
|
||||
/// </summary>
|
||||
public struct SearchResultItem
|
||||
{
|
||||
/// <summary>
|
||||
/// The field the values is for
|
||||
/// </summary>
|
||||
public MessageFieldReference Field { get; set; }
|
||||
/// <summary>
|
||||
/// The value of the field
|
||||
/// </summary>
|
||||
public string? Value { get; set; }
|
||||
}
|
||||
}
|
||||
@ -1,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<T> : JsonConverter<T> where T : new()
|
||||
#endif
|
||||
{
|
||||
private static readonly Lazy<List<ArrayPropertyInfo>> _typePropertyInfo = new Lazy<List<ArrayPropertyInfo>>(CacheTypeAttributes, LazyThreadSafetyMode.PublicationOnly);
|
||||
|
||||
private static SortedDictionary<int, List<ArrayPropertyInfo>>? _typePropertyInfo;
|
||||
|
||||
/// <inheritdoc />
|
||||
#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<ArrayPropertyInfo> CacheTypeAttributes()
|
||||
private static SortedDictionary<int, List<ArrayPropertyInfo>> CacheTypeAttributes()
|
||||
#else
|
||||
private static List<ArrayPropertyInfo> CacheTypeAttributes()
|
||||
private static SortedDictionary<int, List<ArrayPropertyInfo>> CacheTypeAttributes()
|
||||
#endif
|
||||
{
|
||||
var attributes = new List<ArrayPropertyInfo>();
|
||||
var result = new SortedDictionary<int, List<ArrayPropertyInfo>>();
|
||||
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<JsonConverterAttribute>()?.ConverterType ?? targetType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType;
|
||||
attributes.Add(new ArrayPropertyInfo
|
||||
if (!result.TryGetValue(att.Index, out var indexList))
|
||||
{
|
||||
indexList = new List<ArrayPropertyInfo>();
|
||||
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
|
||||
|
||||
@ -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
|
||||
/// <inheritdoc />
|
||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return typeToConvert == typeof(bool) ? new BoolConverterInner<bool>() : new BoolConverterInner<bool?>();
|
||||
return typeToConvert == typeof(bool) ? new BoolConverterInner() : new BoolConverterInnerNullable();
|
||||
}
|
||||
|
||||
private class BoolConverterInner<T> : JsonConverter<T>
|
||||
private class BoolConverterInnerNullable : JsonConverter<bool?>
|
||||
{
|
||||
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<bool>
|
||||
{
|
||||
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> ReadBool(ref reader, typeToConvert, options) ?? false;
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteBooleanValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool? ReadBool(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.True)
|
||||
return true;
|
||||
|
||||
if (reader.TokenType == JsonTokenType.False)
|
||||
return false;
|
||||
|
||||
var value = reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.String => reader.GetString(),
|
||||
JsonTokenType.Number => reader.GetInt16().ToString(),
|
||||
_ => null
|
||||
};
|
||||
|
||||
value = value?.ToLowerInvariant().Trim();
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
if (typeToConvert == typeof(bool))
|
||||
LibraryHelpers.StaticLogger?.LogWarning("Received null or empty bool value, but property type is not a nullable bool. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name);
|
||||
return default;
|
||||
}
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case "true":
|
||||
case "yes":
|
||||
case "y":
|
||||
case "1":
|
||||
case "on":
|
||||
return true;
|
||||
case "false":
|
||||
case "no":
|
||||
case "n":
|
||||
case "0":
|
||||
case "off":
|
||||
case "-1":
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new SerializationException($"Can't convert bool value {value}");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,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;
|
||||
|
||||
|
||||
@ -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
|
||||
/// <inheritdoc />
|
||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner<DateTime>() : new DateTimeConverterInner<DateTime?>();
|
||||
return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner() : new NullableDateTimeConverterInner();
|
||||
}
|
||||
|
||||
private class DateTimeConverterInner<T> : JsonConverter<T>
|
||||
private class NullableDateTimeConverterInner : JsonConverter<DateTime?>
|
||||
{
|
||||
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<DateTime>
|
||||
{
|
||||
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> ReadDateTime(ref reader, typeToConvert, options) ?? default;
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
|
||||
{
|
||||
var dtValue = value;
|
||||
if (dtValue == default)
|
||||
writer.WriteStringValue(default(DateTime));
|
||||
else
|
||||
writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds));
|
||||
}
|
||||
}
|
||||
|
||||
private static DateTime? ReadDateTime(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
{
|
||||
if (typeToConvert == typeof(DateTime))
|
||||
LibraryHelpers.StaticLogger?.LogWarning("DateTime value of null, but property is not nullable. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name);
|
||||
return default;
|
||||
}
|
||||
|
||||
if (reader.TokenType is JsonTokenType.Number)
|
||||
{
|
||||
var decValue = reader.GetDecimal();
|
||||
if (decValue == 0 || decValue < 0)
|
||||
return default;
|
||||
|
||||
return ParseFromDecimal(decValue);
|
||||
}
|
||||
else if (reader.TokenType is JsonTokenType.String)
|
||||
{
|
||||
var stringValue = reader.GetString();
|
||||
if (string.IsNullOrWhiteSpace(stringValue)
|
||||
|| stringValue!.Equals("-1", StringComparison.Ordinal)
|
||||
|| stringValue!.Equals("0001-01-01T00:00:00Z", StringComparison.OrdinalIgnoreCase)
|
||||
|| decimal.TryParse(stringValue, out var decVal) && decVal == 0)
|
||||
{
|
||||
var dtValue = (DateTime)(object)value;
|
||||
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
|
||||
/// </summary>
|
||||
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)
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
|
||||
@ -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<T>, INullableConverterFactory where T : struct, Enum
|
||||
{
|
||||
private static List<KeyValuePair<T, string>>? _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<EnumMapping>? _mappingToEnum = null;
|
||||
private static FrozenDictionary<T, string>? _mappingToString = null;
|
||||
#else
|
||||
private static List<EnumMapping>? _mappingToEnum = null;
|
||||
private static Dictionary<T, string>? _mappingToString = null;
|
||||
#endif
|
||||
private NullableEnumConverter? _nullableEnumConverter = null;
|
||||
|
||||
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
|
||||
@ -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<T, string>)))
|
||||
mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
if (!mapping.Equals(default(KeyValuePair<T, string>)))
|
||||
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<KeyValuePair<T, string>> AddMapping()
|
||||
private static void CreateMapping()
|
||||
{
|
||||
var mapping = new List<KeyValuePair<T, string>>();
|
||||
var mappingToEnum = new List<EnumMapping>();
|
||||
var mappingToString = new Dictionary<T, string>();
|
||||
|
||||
var enumType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||
var enumMembers = enumType.GetFields();
|
||||
foreach (var member in enumMembers)
|
||||
@ -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, string>((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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -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());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -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<T, string>)))
|
||||
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<T, string>)))
|
||||
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
|
||||
{
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
|
||||
@ -0,0 +1,117 @@
|
||||
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON REST message handler
|
||||
/// </summary>
|
||||
public abstract class JsonRestMessageHandler : IRestMessageHandler
|
||||
{
|
||||
private static MediaTypeWithQualityHeaderValue _acceptJsonContent = new MediaTypeWithQualityHeaderValue(Constants.JsonContentHeader);
|
||||
|
||||
/// <summary>
|
||||
/// Empty rate limit error
|
||||
/// </summary>
|
||||
protected static readonly ServerRateLimitError _emptyRateLimitError = new ServerRateLimitError();
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual bool RequiresSeekableStream => false;
|
||||
|
||||
/// <summary>
|
||||
/// The serializer options to use
|
||||
/// </summary>
|
||||
public abstract JsonSerializerOptions Options { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public MediaTypeWithQualityHeaderValue AcceptHeader => _acceptJsonContent;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual ValueTask<ServerRateLimitError> ParseErrorRateLimitResponse(
|
||||
int httpStatusCode,
|
||||
HttpResponseHeaders responseHeaders,
|
||||
Stream responseStream)
|
||||
{
|
||||
// Handle retry after header
|
||||
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
|
||||
if (retryAfterHeader.Value?.Any() != true)
|
||||
return new ValueTask<ServerRateLimitError>(_emptyRateLimitError);
|
||||
|
||||
var value = retryAfterHeader.Value.First();
|
||||
if (int.TryParse(value, out var seconds))
|
||||
return new ValueTask<ServerRateLimitError>(new ServerRateLimitError() { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) });
|
||||
|
||||
if (DateTime.TryParse(value, out var datetime))
|
||||
return new ValueTask<ServerRateLimitError>(new ServerRateLimitError() { RetryAfter = datetime });
|
||||
|
||||
return new ValueTask<ServerRateLimitError>(_emptyRateLimitError);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract ValueTask<Error> ParseErrorResponse(
|
||||
int httpStatusCode,
|
||||
HttpResponseHeaders responseHeaders,
|
||||
Stream responseStream);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual ValueTask<Error?> CheckForErrorResponse(
|
||||
RequestDefinition request,
|
||||
HttpResponseHeaders responseHeaders,
|
||||
Stream responseStream) => new ValueTask<Error?>((Error?)null);
|
||||
|
||||
/// <summary>
|
||||
/// Read the response into a JsonDocument object
|
||||
/// </summary>
|
||||
protected virtual async ValueTask<(Error?, JsonDocument?)> GetJsonDocument(Stream stream)
|
||||
{
|
||||
try
|
||||
{
|
||||
var document = await JsonDocument.ParseAsync(stream).ConfigureAwait(false);
|
||||
return (null, document);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (new ServerError(new ErrorInfo(ErrorType.DeserializationFailed, false, "Deserialization failed, invalid JSON"), ex), null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public async ValueTask<(T? Result, Error? Error)> TryDeserializeAsync<T>(Stream responseStream, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||
var result = await JsonSerializer.DeserializeAsync<T>(responseStream, Options)!.ConfigureAwait(false)!;
|
||||
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
return (result, null);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
||||
return (default, new DeserializeError(info, ex));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (default, new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Error? CheckDeserializedResponse<T>(HttpResponseHeaders responseHeaders, T result) => null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,348 @@
|
||||
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON WebSocket message handler, sequentially read the JSON and looks for specific predefined fields to identify the message
|
||||
/// </summary>
|
||||
public abstract class JsonSocketMessageHandler : ISocketMessageHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// The serializer options to use
|
||||
/// </summary>
|
||||
public abstract JsonSerializerOptions Options { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Message evaluators
|
||||
/// </summary>
|
||||
protected abstract MessageTypeDefinition[] TypeEvaluators { get; }
|
||||
|
||||
private readonly SearchResult _searchResult = new();
|
||||
|
||||
private bool _hasArraySearches;
|
||||
private bool _initialized;
|
||||
private int _maxSearchDepth;
|
||||
private MessageTypeDefinition? _topEvaluator;
|
||||
private List<MessageEvalutorFieldReference>? _searchFields;
|
||||
private Dictionary<Type, Func<object, string?>>? _baseTypeMapping;
|
||||
private Dictionary<Type, Func<object, string?>>? _mapping;
|
||||
|
||||
/// <summary>
|
||||
/// Add a mapping of a specific object of a type to a specific topic
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type to get topic for</typeparam>
|
||||
/// <param name="mapping">The topic retrieve delegate</param>
|
||||
protected void AddTopicMapping<T>(Func<T, string?> mapping)
|
||||
{
|
||||
_mapping ??= new Dictionary<Type, Func<object, string?>>();
|
||||
_mapping.Add(typeof(T), x => mapping((T)x));
|
||||
}
|
||||
|
||||
private void InitializeConverter()
|
||||
{
|
||||
if (_initialized)
|
||||
return;
|
||||
|
||||
_maxSearchDepth = int.MinValue;
|
||||
_searchFields = new List<MessageEvalutorFieldReference>();
|
||||
foreach (var evaluator in TypeEvaluators)
|
||||
{
|
||||
_topEvaluator ??= evaluator;
|
||||
foreach (var field in evaluator.Fields)
|
||||
{
|
||||
var overlapping = _searchFields.Where(otherField =>
|
||||
{
|
||||
if (field is PropertyFieldReference propRef
|
||||
&& otherField.Field is PropertyFieldReference otherPropRef)
|
||||
{
|
||||
return field.Depth == otherPropRef.Depth && propRef.PropertyName.SequenceEqual(otherPropRef.PropertyName);
|
||||
}
|
||||
else if (field is ArrayFieldReference arrayRef
|
||||
&& otherField.Field is ArrayFieldReference otherArrayPropRef)
|
||||
{
|
||||
return field.Depth == otherArrayPropRef.Depth && arrayRef.ArrayIndex == otherArrayPropRef.ArrayIndex;
|
||||
}
|
||||
|
||||
return false;
|
||||
}).ToList();
|
||||
|
||||
if (overlapping.Any())
|
||||
{
|
||||
foreach (var overlap in overlapping)
|
||||
overlap.OverlappingField = true;
|
||||
}
|
||||
|
||||
List<MessageEvalutorFieldReference>? existingSameSearchField = new();
|
||||
if (field is ArrayFieldReference arrayField)
|
||||
{
|
||||
_hasArraySearches = true;
|
||||
existingSameSearchField = _searchFields.Where(x =>
|
||||
x.Field is ArrayFieldReference arrayFieldRef
|
||||
&& arrayFieldRef.ArrayIndex == arrayField.ArrayIndex
|
||||
&& arrayFieldRef.Depth == arrayField.Depth
|
||||
&& arrayFieldRef.Constraint == null && arrayField.Constraint == null).ToList();
|
||||
}
|
||||
else if (field is PropertyFieldReference propField)
|
||||
{
|
||||
existingSameSearchField = _searchFields.Where(x =>
|
||||
x.Field is PropertyFieldReference propFieldRef
|
||||
&& propFieldRef.PropertyName.SequenceEqual(propField.PropertyName)
|
||||
&& propFieldRef.Depth == propField.Depth
|
||||
&& propFieldRef.Constraint == null && propFieldRef.Constraint == null).ToList();
|
||||
}
|
||||
|
||||
foreach(var sameSearchField in existingSameSearchField)
|
||||
{
|
||||
if (sameSearchField.SkipReading == true
|
||||
&& (evaluator.TypeIdentifierCallback != null || field.Constraint != null))
|
||||
{
|
||||
sameSearchField.SkipReading = false;
|
||||
}
|
||||
|
||||
if (evaluator.ForceIfFound)
|
||||
{
|
||||
if (evaluator.Fields.Length > 1 || sameSearchField.ForceEvaluator != null)
|
||||
throw new Exception("Invalid config");
|
||||
|
||||
//sameSearchField.ForceEvaluator = evaluator;
|
||||
}
|
||||
}
|
||||
|
||||
_searchFields.Add(new MessageEvalutorFieldReference(field)
|
||||
{
|
||||
SkipReading = evaluator.TypeIdentifierCallback == null && field.Constraint == null,
|
||||
ForceEvaluator = !existingSameSearchField.Any() ? evaluator.ForceIfFound ? evaluator : null : null,
|
||||
OverlappingField = overlapping.Any()
|
||||
});
|
||||
|
||||
if (field.Depth > _maxSearchDepth)
|
||||
_maxSearchDepth = field.Depth;
|
||||
}
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual string? GetTopicFilter(object deserializedObject)
|
||||
{
|
||||
if (_mapping == null)
|
||||
return null;
|
||||
|
||||
// Cache the found type for future
|
||||
var currentType = deserializedObject.GetType();
|
||||
if (_baseTypeMapping != null)
|
||||
{
|
||||
if (_baseTypeMapping.TryGetValue(currentType, out var typeMapping))
|
||||
return typeMapping(deserializedObject);
|
||||
}
|
||||
|
||||
var mappedBase = false;
|
||||
while (currentType != null)
|
||||
{
|
||||
if (_mapping.TryGetValue(currentType, out var mapping))
|
||||
{
|
||||
if (mappedBase)
|
||||
{
|
||||
_baseTypeMapping ??= new Dictionary<Type, Func<object, string?>>();
|
||||
_baseTypeMapping.Add(deserializedObject.GetType(), mapping);
|
||||
}
|
||||
|
||||
return mapping(deserializedObject);
|
||||
}
|
||||
|
||||
mappedBase = true;
|
||||
currentType = currentType.BaseType;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
|
||||
{
|
||||
InitializeConverter();
|
||||
|
||||
int? arrayIndex = null;
|
||||
|
||||
_searchResult.Clear();
|
||||
var reader = new Utf8JsonReader(data);
|
||||
while (reader.Read())
|
||||
{
|
||||
if ((reader.TokenType == JsonTokenType.StartArray
|
||||
|| reader.TokenType == JsonTokenType.StartObject)
|
||||
&& reader.CurrentDepth == _maxSearchDepth)
|
||||
{
|
||||
// There is no field we need to search for on a depth deeper than this, skip
|
||||
reader.Skip();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (reader.TokenType == JsonTokenType.StartArray)
|
||||
arrayIndex = -1;
|
||||
else if (reader.TokenType == JsonTokenType.EndArray)
|
||||
arrayIndex = null;
|
||||
else if (arrayIndex != null)
|
||||
arrayIndex++;
|
||||
|
||||
if (reader.TokenType == JsonTokenType.PropertyName
|
||||
|| arrayIndex != null && _hasArraySearches)
|
||||
{
|
||||
bool written = false;
|
||||
|
||||
string? value = null;
|
||||
byte[]? propName = null;
|
||||
foreach (var field in _searchFields!)
|
||||
{
|
||||
if (field.Field.Depth != reader.CurrentDepth)
|
||||
continue;
|
||||
|
||||
bool readArrayValues = false;
|
||||
if (field.Field is PropertyFieldReference propFieldRef)
|
||||
{
|
||||
if (propName == null)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.PropertyName)
|
||||
continue;
|
||||
|
||||
if (!reader.ValueTextEquals(propFieldRef.PropertyName))
|
||||
continue;
|
||||
|
||||
propName = propFieldRef.PropertyName;
|
||||
readArrayValues = propFieldRef.ArrayValues;
|
||||
reader.Read();
|
||||
}
|
||||
else if (!propFieldRef.PropertyName.SequenceEqual(propName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (field.Field is ArrayFieldReference arrayFieldRef)
|
||||
{
|
||||
if (propName != null)
|
||||
continue;
|
||||
|
||||
if (reader.TokenType == JsonTokenType.PropertyName)
|
||||
continue;
|
||||
|
||||
if (arrayFieldRef.ArrayIndex != arrayIndex)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!field.SkipReading)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
if (readArrayValues)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartArray)
|
||||
// error
|
||||
return null;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
reader.Read();// Read start array
|
||||
bool first = true;
|
||||
while(reader.TokenType != JsonTokenType.EndArray)
|
||||
{
|
||||
if (!first)
|
||||
sb.Append(",");
|
||||
|
||||
first = false;
|
||||
sb.Append(reader.GetString());
|
||||
reader.Read();
|
||||
}
|
||||
|
||||
value = first ? null : sb.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonTokenType.Number:
|
||||
value = reader.GetDecimal().ToString();
|
||||
break;
|
||||
case JsonTokenType.String:
|
||||
value = reader.GetString()!;
|
||||
break;
|
||||
case JsonTokenType.True:
|
||||
case JsonTokenType.False:
|
||||
value = reader.GetBoolean().ToString()!;
|
||||
break;
|
||||
case JsonTokenType.Null:
|
||||
value = null;
|
||||
break;
|
||||
case JsonTokenType.StartObject:
|
||||
case JsonTokenType.StartArray:
|
||||
value = null;
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (field.Field.Constraint != null
|
||||
&& !field.Field.Constraint(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
_searchResult.Write(field.Field, value);
|
||||
|
||||
if (field.ForceEvaluator != null)
|
||||
{
|
||||
if (field.ForceEvaluator.StaticIdentifier != null)
|
||||
return field.ForceEvaluator.StaticIdentifier;
|
||||
|
||||
// Force the immediate return upon encountering this field
|
||||
return field.ForceEvaluator.GetMessageType(_searchResult);
|
||||
}
|
||||
|
||||
written = true;
|
||||
if (!field.OverlappingField)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!written)
|
||||
continue;
|
||||
|
||||
if (_topEvaluator!.Satisfied(_searchResult))
|
||||
return _topEvaluator.GetMessageType(_searchResult);
|
||||
|
||||
if (_searchFields.Count == _searchResult.Count)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var evaluator in TypeEvaluators)
|
||||
{
|
||||
if (evaluator.Satisfied(_searchResult))
|
||||
return evaluator.GetMessageType(_searchResult);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public virtual object Deserialize(ReadOnlySpan<byte> data, Type type)
|
||||
{
|
||||
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||
return JsonSerializer.Deserialize(data, type, Options)!;
|
||||
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||
using System;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON WebSocket message handler, reads the json data info a JsonDocument after which the data can be inspected to identify the message
|
||||
/// </summary>
|
||||
public abstract class JsonSocketPreloadMessageHandler : ISocketMessageHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// The serializer options to use
|
||||
/// </summary>
|
||||
public abstract JsonSerializerOptions Options { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
|
||||
{
|
||||
var reader = new Utf8JsonReader(data);
|
||||
var jsonDocument = JsonDocument.ParseValue(ref reader);
|
||||
|
||||
return GetTypeIdentifier(jsonDocument);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the message identifier for this document
|
||||
/// </summary>
|
||||
protected abstract string? GetTypeIdentifier(JsonDocument document);
|
||||
|
||||
/// <summary>
|
||||
/// Get optional topic filter, for example a symbol name
|
||||
/// </summary>
|
||||
public virtual string? GetTopicFilter(object deserializedObject) => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual object Deserialize(ReadOnlySpan<byte> data, Type type)
|
||||
{
|
||||
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||
return JsonSerializer.Deserialize(data, type, Options)!;
|
||||
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the string value for a path, or an emtpy string if not found
|
||||
/// </summary>
|
||||
protected string StringOrEmpty(JsonDocument document, string path)
|
||||
{
|
||||
if (!document.RootElement.TryGetProperty(path, out var element))
|
||||
return string.Empty;
|
||||
|
||||
if (element.ValueKind == JsonValueKind.String)
|
||||
return element.GetString() ?? string.Empty;
|
||||
else if (element.ValueKind == JsonValueKind.Number)
|
||||
return element.GetDecimal().ToString();
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
@ -6,9 +6,9 @@
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||
<PackageVersion>9.13.0</PackageVersion>
|
||||
<AssemblyVersion>9.13.0</AssemblyVersion>
|
||||
<FileVersion>9.13.0</FileVersion>
|
||||
<PackageVersion>10.0.0</PackageVersion>
|
||||
<AssemblyVersion>10.0.0</AssemblyVersion>
|
||||
<FileVersion>10.0.0</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
@ -20,7 +20,7 @@
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
@ -45,18 +45,19 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.100">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.101">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.1" />
|
||||
<PackageReference Include="System.Text.Json" Version="10.0.1" />
|
||||
<PackageReference Include="NSec.Cryptography" Version="25.4.0" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Transitive Client Packages">
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" />
|
||||
<PackageReference Include="System.Threading.Channels" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
|
||||
<PackageReference Include="System.Threading.Channels" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
24
CryptoExchange.Net/Exceptions/CeDeserializationException.cs
Normal file
24
CryptoExchange.Net/Exceptions/CeDeserializationException.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Exception during deserialization
|
||||
/// </summary>
|
||||
public class CeDeserializationException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public CeDeserializationException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public CeDeserializationException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue updates and process them async
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The queued update type</typeparam>
|
||||
/// <param name="subscribeCall">The subscribe call</param>
|
||||
/// <param name="asyncHandler">The async update handler</param>
|
||||
/// <param name="maxQueuedItems">The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with <see>fullMode</see></param>
|
||||
/// <param name="fullBehavior">What should happen if the queue contains <see>maxQueuedItems</see> pending updates. If no max is set this setting is ignored</param>
|
||||
/// <param name="ct">Cancellation token to stop the processing</param>
|
||||
public static async Task ProcessQueuedAsync<T>(
|
||||
Func<Action<T>, Task> subscribeCall,
|
||||
Func<T, Task> asyncHandler,
|
||||
CancellationToken ct,
|
||||
int? maxQueuedItems = null,
|
||||
QueueFullBehavior? fullBehavior = null)
|
||||
{
|
||||
var processor = new ProcessQueue<T>(asyncHandler, maxQueuedItems, fullBehavior);
|
||||
await processor.StartAsync().ConfigureAwait(false);
|
||||
ct.Register(async () =>
|
||||
{
|
||||
await processor.StopAsync().ConfigureAwait(false);
|
||||
});
|
||||
|
||||
await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue updates received from a websocket subscriptions and process them async
|
||||
/// </summary>
|
||||
@ -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);
|
||||
};
|
||||
|
||||
|
||||
@ -3,7 +3,6 @@ using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
|
||||
@ -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
|
||||
/// <returns></returns>
|
||||
public static string CreateParamString(this IDictionary<string, object> 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<object>().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<object>().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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -233,18 +271,16 @@ namespace CryptoExchange.Net
|
||||
/// <summary>
|
||||
/// Append a base url with provided path
|
||||
/// </summary>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -366,19 +402,40 @@ namespace CryptoExchange.Net
|
||||
/// <summary>
|
||||
/// Decompress using GzipStream
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
public static ReadOnlySpan<byte> DecompressGzip(this ReadOnlySpan<byte> data)
|
||||
{
|
||||
using var decompressedStream = new MemoryStream();
|
||||
using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress);
|
||||
deflateStream.CopyTo(decompressedStream);
|
||||
return new ReadOnlySpan<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompress using GzipStream
|
||||
/// </summary>
|
||||
public static ReadOnlyMemory<byte> DecompressGzip(this ReadOnlyMemory<byte> data)
|
||||
{
|
||||
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<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompress using GzipStream
|
||||
/// </summary>
|
||||
public static ReadOnlySpan<byte> Decompress(this ReadOnlySpan<byte> input)
|
||||
{
|
||||
using var output = new MemoryStream();
|
||||
using var compressStream = new MemoryStream(input.ToArray());
|
||||
using var decompressor = new DeflateStream(compressStream, CompressionMode.Decompress);
|
||||
decompressor.CopyTo(output);
|
||||
return new ReadOnlySpan<byte>(output.GetBuffer(), 0, (int)output.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompress using DeflateStream
|
||||
/// </summary>
|
||||
@ -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<byte>(output.GetBuffer(), 0, (int)output.Length);
|
||||
}
|
||||
|
||||
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Base api client
|
||||
@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
namespace CryptoExchange.Net.Interfaces.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// Client for accessing REST API's for different exchanges
|
||||
@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
namespace CryptoExchange.Net.Interfaces.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// Client for accessing Websocket API's for different exchanges
|
||||
@ -1,4 +1,4 @@
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
namespace CryptoExchange.Net.Interfaces.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// Base rest API client
|
||||
@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
namespace CryptoExchange.Net.Interfaces.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for rest API implementations
|
||||
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Socket API client
|
||||
@ -27,6 +29,10 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
IWebsocketFactory SocketFactory { get; set; }
|
||||
/// <summary>
|
||||
/// High performance websocket factory
|
||||
/// </summary>
|
||||
IHighPerfConnectionFactory? HighPerfConnectionFactory { get; set; }
|
||||
/// <summary>
|
||||
/// Current client options
|
||||
/// </summary>
|
||||
SocketExchangeOptions ClientOptions { get; }
|
||||
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for socket API implementations
|
||||
@ -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;
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Serializer interface
|
||||
|
||||
@ -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
|
||||
/// <summary>
|
||||
/// Accept header
|
||||
/// </summary>
|
||||
string Accept { set; }
|
||||
MediaTypeWithQualityHeaderValue Accept { set; }
|
||||
/// <summary>
|
||||
/// Content
|
||||
/// </summary>
|
||||
@ -58,7 +58,7 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// Get all headers
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
KeyValuePair<string, string[]>[] GetHeaders();
|
||||
HttpRequestHeaders GetHeaders();
|
||||
|
||||
/// <summary>
|
||||
/// Get the response
|
||||
|
||||
@ -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
|
||||
/// <summary>
|
||||
/// The response headers
|
||||
/// </summary>
|
||||
KeyValuePair<string, string[]>[] ResponseHeaders { get; }
|
||||
HttpResponseHeaders ResponseHeaders { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the response stream
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Objects;
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Websocket factory interface
|
||||
/// </summary>
|
||||
public interface IWebsocketFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a websocket for an url
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger</param>
|
||||
/// <param name="parameters">The parameters to use for the connection</param>
|
||||
/// <returns></returns>
|
||||
IWebsocket CreateWebsocket(ILogger logger, WebSocketParameters parameters);
|
||||
}
|
||||
}
|
||||
@ -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))
|
||||
|
||||
@ -22,6 +22,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
private static readonly Action<ILogger, string, Exception?> _restApiCacheHit;
|
||||
private static readonly Action<ILogger, string, Exception?> _restApiCacheNotHit;
|
||||
private static readonly Action<ILogger, int?, Exception?> _restApiCancellationRequested;
|
||||
private static readonly Action<ILogger, int?, string?, Exception?> _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<int?, string?>(
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<ILogger, int, bool, Exception?> _activityPaused;
|
||||
private static readonly Action<ILogger, int, Sockets.SocketConnection.SocketStatus, Sockets.SocketConnection.SocketStatus, Exception?> _socketStatusChanged;
|
||||
private static readonly Action<ILogger, int, SocketStatus, SocketStatus, Exception?> _socketStatusChanged;
|
||||
private static readonly Action<ILogger, int, string?, Exception?> _failedReconnectProcessing;
|
||||
private static readonly Action<ILogger, int, Exception?> _unknownExceptionWhileProcessingReconnection;
|
||||
private static readonly Action<ILogger, int, WebSocketError, string?, Exception?> _webSocketErrorCodeAndDetails;
|
||||
@ -46,7 +47,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
new EventId(2000, "ActivityPaused"),
|
||||
"[Sckt {SocketId}] paused activity: {Paused}");
|
||||
|
||||
_socketStatusChanged = LoggerMessage.Define<int, Sockets.SocketConnection.SocketStatus, Sockets.SocketConnection.SocketStatus>(
|
||||
_socketStatusChanged = LoggerMessage.Define<int, SocketStatus, SocketStatus>(
|
||||
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);
|
||||
}
|
||||
|
||||
@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// An alias used by the exchange for an asset commonly known by another name
|
||||
|
||||
@ -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
|
||||
/// <summary>
|
||||
/// Map the common name to an exchange name for an asset. If there is no alias the input name is returned
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
|
||||
|
||||
@ -14,6 +14,11 @@ namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
private static readonly Task<bool> _completed = Task.FromResult(true);
|
||||
private Queue<TaskCompletionSource<bool>> _waits = new Queue<TaskCompletionSource<bool>>();
|
||||
#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<bool> 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
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
/// <summary>
|
||||
/// The headers sent with the request
|
||||
/// </summary>
|
||||
public KeyValuePair<string, string[]>[]? RequestHeaders { get; set; }
|
||||
public HttpRequestHeaders? RequestHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The request id
|
||||
@ -244,7 +244,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// The response headers
|
||||
/// </summary>
|
||||
public KeyValuePair<string, string[]>[]? ResponseHeaders { get; set; }
|
||||
public HttpResponseHeaders? ResponseHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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<string, string[]>[]? responseHeaders,
|
||||
HttpResponseHeaders? responseHeaders,
|
||||
TimeSpan? responseTime,
|
||||
string? originalData,
|
||||
int? requestId,
|
||||
string? requestUrl,
|
||||
string? requestBody,
|
||||
HttpMethod? requestMethod,
|
||||
KeyValuePair<string, string[]>[]? requestHeaders,
|
||||
HttpRequestHeaders? requestHeaders,
|
||||
Error? error) : base(error)
|
||||
{
|
||||
ResponseStatusCode = code;
|
||||
@ -370,7 +370,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// The headers sent with the request
|
||||
/// </summary>
|
||||
public KeyValuePair<string, string[]>[]? RequestHeaders { get; set; }
|
||||
public HttpRequestHeaders? RequestHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The request id
|
||||
@ -400,7 +400,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// The response headers
|
||||
/// </summary>
|
||||
public KeyValuePair<string, string[]>[]? ResponseHeaders { get; set; }
|
||||
public HttpResponseHeaders? ResponseHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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<string, string[]>[]? responseHeaders,
|
||||
HttpResponseHeaders? responseHeaders,
|
||||
TimeSpan? responseTime,
|
||||
long? responseLength,
|
||||
string? originalData,
|
||||
@ -426,7 +426,7 @@ namespace CryptoExchange.Net.Objects
|
||||
string? requestUrl,
|
||||
string? requestBody,
|
||||
HttpMethod? requestMethod,
|
||||
KeyValuePair<string, string[]>[]? requestHeaders,
|
||||
HttpRequestHeaders? requestHeaders,
|
||||
ResultDataSource dataSource,
|
||||
[AllowNull] T data,
|
||||
Error? error) : base(data, originalData, error)
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
using CryptoExchange.Net.Attributes;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// What to do when a request would exceed the rate limit
|
||||
|
||||
@ -79,7 +79,20 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Errors
|
||||
{
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Errors
|
||||
namespace CryptoExchange.Net.Objects.Errors
|
||||
{
|
||||
/// <summary>
|
||||
/// Error info
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Errors
|
||||
{
|
||||
|
||||
@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Errors
|
||||
namespace CryptoExchange.Net.Objects.Errors
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of error
|
||||
|
||||
@ -8,7 +8,8 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
public class ApiOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
public bool? OutputOriginalData { get; set; }
|
||||
|
||||
|
||||
@ -14,7 +14,8 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
public ApiProxy? Proxy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
public bool OutputOriginalData { get; set; } = false;
|
||||
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
|
||||
@ -32,10 +32,24 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// <para>
|
||||
/// This setting counts each Subscribe request as one instead of counting the individual subscriptions as <see cref="SocketIndividualSubscriptionCombineTarget"/> does
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public int? SocketSubscriptionsCombineTarget { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// <para>
|
||||
/// This setting counts the individual subscriptions in a request instead of counting subscriptions in batched request as one as <see cref="SocketSubscriptionsCombineTarget"/> does.
|
||||
/// </para>
|
||||
/// <para>Defaults to 20</para>
|
||||
/// </summary>
|
||||
public int SocketIndividualSubscriptionCombineTarget { get; set; } = 20;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
@ -61,6 +75,11 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// </remarks>
|
||||
public int? ReceiveBufferSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to use the updated deserialization logic, default is true
|
||||
/// </summary>
|
||||
public bool UseUpdatedDeserialization { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
@ -82,6 +101,7 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
item.RateLimitingBehaviour = RateLimitingBehaviour;
|
||||
item.RateLimiterEnabled = RateLimiterEnabled;
|
||||
item.ReceiveBufferSize = ReceiveBufferSize;
|
||||
item.UseUpdatedDeserialization = UseUpdatedDeserialization;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
|
||||
@ -13,6 +13,15 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public class ParameterCollection : Dictionary<string, object>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public new void Add(string key, object value)
|
||||
{
|
||||
if (value == null)
|
||||
throw new ArgumentNullException(key);
|
||||
|
||||
base.Add(key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an optional parameter. Not added if value is null
|
||||
/// </summary>
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -31,7 +40,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="value"></param>
|
||||
public void AddString(string key, decimal value)
|
||||
{
|
||||
Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
base.Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -52,7 +61,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="value"></param>
|
||||
public void AddString(string key, int value)
|
||||
{
|
||||
Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
base.Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -73,7 +82,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="value"></param>
|
||||
public void AddString(string key, long value)
|
||||
{
|
||||
Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
base.Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -94,7 +103,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="value"></param>
|
||||
public void AddMilliseconds(string key, DateTime value)
|
||||
{
|
||||
Add(key, DateTimeConverter.ConvertToMilliseconds(value));
|
||||
base.Add(key, DateTimeConverter.ConvertToMilliseconds(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -115,7 +124,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="value"></param>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -136,7 +145,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="value"></param>
|
||||
public void AddSeconds(string key, DateTime value)
|
||||
{
|
||||
Add(key, DateTimeConverter.ConvertToSeconds(value));
|
||||
base.Add(key, DateTimeConverter.ConvertToSeconds(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -157,7 +166,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="value"></param>
|
||||
public void AddSecondsString(string key, DateTime value)
|
||||
{
|
||||
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
|
||||
base.Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -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()!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -181,7 +190,7 @@ namespace CryptoExchange.Net.Objects
|
||||
#endif
|
||||
where T : struct, Enum
|
||||
{
|
||||
Add(key, EnumConverter<T>.GetString(value)!);
|
||||
base.Add(key, EnumConverter<T>.GetString(value)!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -197,7 +206,7 @@ namespace CryptoExchange.Net.Objects
|
||||
where T : struct, Enum
|
||||
{
|
||||
var stringVal = EnumConverter<T>.GetString(value)!;
|
||||
Add(key, int.Parse(stringVal)!);
|
||||
base.Add(key, int.Parse(stringVal)!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -213,7 +222,7 @@ namespace CryptoExchange.Net.Objects
|
||||
where T : struct, Enum
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, EnumConverter<T>.GetString(value));
|
||||
base.Add(key, EnumConverter<T>.GetString(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -229,7 +238,7 @@ namespace CryptoExchange.Net.Objects
|
||||
if (value != null)
|
||||
{
|
||||
var stringVal = EnumConverter<T>.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
@ -30,15 +30,15 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// Query parameters
|
||||
/// </summary>
|
||||
public IDictionary<string, object> QueryParameters { get; set; }
|
||||
public IDictionary<string, object>? QueryParameters { get; set; }
|
||||
/// <summary>
|
||||
/// Body parameters
|
||||
/// </summary>
|
||||
public IDictionary<string, object> BodyParameters { get; set; }
|
||||
public IDictionary<string, object>? BodyParameters { get; set; }
|
||||
/// <summary>
|
||||
/// Request headers
|
||||
/// </summary>
|
||||
public IDictionary<string, string> Headers { get; set; }
|
||||
public IDictionary<string, string>? Headers { get; set; }
|
||||
/// <summary>
|
||||
/// Array serialization type
|
||||
/// </summary>
|
||||
@ -58,9 +58,9 @@ namespace CryptoExchange.Net.Objects
|
||||
public RestRequestConfiguration(
|
||||
RequestDefinition requestDefinition,
|
||||
string baseAddress,
|
||||
IDictionary<string, object> queryParams,
|
||||
IDictionary<string, object> bodyParams,
|
||||
IDictionary<string, string> headers,
|
||||
IDictionary<string, object>? queryParams,
|
||||
IDictionary<string, object>? bodyParams,
|
||||
IDictionary<string, string>? headers,
|
||||
ArrayParametersSerialization arraySerialization,
|
||||
HttpMethodParameterPosition parametersPosition,
|
||||
RequestBodyFormat bodyFormat)
|
||||
@ -83,8 +83,12 @@ namespace CryptoExchange.Net.Objects
|
||||
public IDictionary<string, object> GetPositionParameters()
|
||||
{
|
||||
if (ParameterPosition == HttpMethodParameterPosition.InBody)
|
||||
{
|
||||
BodyParameters ??= new Dictionary<string, object>();
|
||||
return BodyParameters;
|
||||
}
|
||||
|
||||
QueryParameters ??= new Dictionary<string, object>();
|
||||
return QueryParameters;
|
||||
}
|
||||
|
||||
@ -94,7 +98,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="urlEncode">Whether to URL encode the parameter string if creating new</param>
|
||||
public string GetQueryString(bool urlEncode = true)
|
||||
{
|
||||
return _queryString ?? QueryParameters.CreateParamString(urlEncode, ArraySerialization);
|
||||
return _queryString ?? QueryParameters?.CreateParamString(urlEncode, ArraySerialization) ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -6,8 +6,7 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// <summary>
|
||||
/// An update received from a socket update subscription
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the data</typeparam>
|
||||
public class DataEvent<T>
|
||||
public class DataEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The timestamp the data was received
|
||||
@ -29,6 +28,11 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// </summary>
|
||||
public string? Symbol { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The exchange name
|
||||
/// </summary>
|
||||
public string Exchange { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The original data that was received, only available when OutputOriginalData is set to true in the client options
|
||||
/// </summary>
|
||||
@ -39,6 +43,29 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// </summary>
|
||||
public SocketUpdateType? UpdateType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public DataEvent(
|
||||
string exchange,
|
||||
DateTime receiveTimestamp,
|
||||
string? originalData)
|
||||
{
|
||||
Exchange = exchange;
|
||||
OriginalData = originalData;
|
||||
ReceiveTime = receiveTimestamp;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{StreamId} - {(Symbol == null ? "" : (Symbol + " - "))}{UpdateType}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public class DataEvent<T> : DataEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The received data deserialized into an object
|
||||
/// </summary>
|
||||
@ -47,75 +74,13 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The type of the new data</typeparam>
|
||||
/// <param name="data">The new data</param>
|
||||
/// <returns></returns>
|
||||
public DataEvent<K> As<K>(K data)
|
||||
{
|
||||
return new DataEvent<K>(data, StreamId, Symbol, OriginalData, ReceiveTime, UpdateType)
|
||||
{
|
||||
DataTime = DataTime
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new DataEvent with data in the from of type K based on the current DataEvent. OriginalData and ReceivedTimestamp will be copied over
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The type of the new data</typeparam>
|
||||
/// <param name="data">The new data</param>
|
||||
/// <param name="symbol">The new symbol</param>
|
||||
/// <returns></returns>
|
||||
public DataEvent<K> As<K>(K data, string? symbol)
|
||||
{
|
||||
return new DataEvent<K>(data, StreamId, symbol, OriginalData, ReceiveTime, UpdateType)
|
||||
{
|
||||
DataTime = DataTime
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new DataEvent with data in the from of type K based on the current DataEvent. OriginalData and ReceivedTimestamp will be copied over
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The type of the new data</typeparam>
|
||||
/// <param name="data">The new data</param>
|
||||
/// <param name="streamId">The new stream id</param>
|
||||
/// <param name="symbol">The new symbol</param>
|
||||
/// <param name="updateType">The type of update</param>
|
||||
/// <returns></returns>
|
||||
public DataEvent<K> As<K>(K data, string streamId, string? symbol, SocketUpdateType updateType)
|
||||
{
|
||||
return new DataEvent<K>(data, streamId, symbol, OriginalData, ReceiveTime, updateType)
|
||||
{
|
||||
DataTime = DataTime
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The new type</typeparam>
|
||||
/// <param name="exchange">The exchange the result is for</param>
|
||||
/// <param name="data">The data</param>
|
||||
/// <returns></returns>
|
||||
public ExchangeEvent<K> AsExchangeEvent<K>(string exchange, K data)
|
||||
{
|
||||
return new ExchangeEvent<K>(exchange, this.As<K>(data))
|
||||
{
|
||||
DataTime = DataTime
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -123,7 +88,7 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// </summary>
|
||||
/// <param name="symbol"></param>
|
||||
/// <returns></returns>
|
||||
public DataEvent<T> WithSymbol(string symbol)
|
||||
public DataEvent<T> WithSymbol(string? symbol)
|
||||
{
|
||||
Symbol = symbol;
|
||||
return this;
|
||||
@ -161,36 +126,19 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a CallResult from this DataEvent
|
||||
/// Create a new DataEvent of the new type
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public CallResult<T> ToCallResult()
|
||||
public DataEvent<TNew> ToType<TNew>(TNew data)
|
||||
{
|
||||
return new CallResult<T>(Data, OriginalData, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a CallResult from this DataEvent
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public CallResult<K> ToCallResult<K>(K data)
|
||||
{
|
||||
return new CallResult<K>(data, OriginalData, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a CallResult from this DataEvent
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public CallResult<K> ToCallResult<K>(Error error)
|
||||
{
|
||||
return new CallResult<K>(default, OriginalData, error);
|
||||
return new DataEvent<TNew>(Exchange, data, ReceiveTime, OriginalData)
|
||||
{
|
||||
StreamId = StreamId,
|
||||
UpdateType = UpdateType,
|
||||
Symbol = Symbol
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{StreamId} - {(Symbol == null ? "" : (Symbol + " - "))}{(UpdateType == null ? "" : (UpdateType + " - "))}{Data}";
|
||||
}
|
||||
public override string ToString() => base.ToString().TrimEnd('-') + Data?.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
101
CryptoExchange.Net/Objects/Sockets/HighPerfUpdateSubscription.cs
Normal file
101
CryptoExchange.Net/Objects/Sockets/HighPerfUpdateSubscription.cs
Normal file
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Subscription to a data stream
|
||||
/// </summary>
|
||||
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<Action> _connectionClosedEventHandlers = new List<Action>();
|
||||
|
||||
/// <summary>
|
||||
/// Event when the connection is closed and will not be reconnected
|
||||
/// </summary>
|
||||
public event Action ConnectionClosed
|
||||
{
|
||||
add { lock (_eventLock) _connectionClosedEventHandlers.Add(value); }
|
||||
remove { lock (_eventLock) _connectionClosedEventHandlers.Remove(value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event when an exception happens during the handling of the data
|
||||
/// </summary>
|
||||
public event Action<Exception> Exception
|
||||
{
|
||||
add => _subscription.Exception += value;
|
||||
remove => _subscription.Exception -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The id of the socket
|
||||
/// </summary>
|
||||
public int SocketId => _connection.SocketId;
|
||||
|
||||
/// <summary>
|
||||
/// The id of the subscription
|
||||
/// </summary>
|
||||
public int Id => _subscription.Id;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="connection">The socket connection the subscription is on</param>
|
||||
/// <param name="subscription">The subscription</param>
|
||||
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<Action> handlers;
|
||||
lock (_eventLock)
|
||||
handlers = _connectionClosedEventHandlers.ToList();
|
||||
|
||||
foreach(var callback in handlers)
|
||||
callback();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Close the subscription
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task CloseAsync()
|
||||
{
|
||||
return _connection.CloseAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<Action> _connectionClosedEventHandlers = new List<Action>();
|
||||
private List<Action> _connectionLostEventHandlers = new List<Action>();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user