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

Compare commits

..

38 Commits

Author SHA1 Message Date
JKorf d2dc8a06e5 Updated version 2024-02-06 19:31:23 +01:00
JKorf 9ddd446892 Some adjustment to the Crypto clients 2024-02-06 19:28:31 +01:00
JKorf 1eb0214e7a Wip docs 2024-02-06 16:26:42 +01:00
JKorf 4e4ffcab1c Corrected ICryptoRestClient and ICryptoSocketClient namespaces 2024-02-06 16:25:27 +01:00
JKorf 0d6a100aac Update README.md 2024-02-06 14:55:26 +01:00
JKorf 99df7cc792 comments 2024-02-04 17:42:10 +01:00
JKorf 3fa8277a30 Small refactor QueryPeriodic to only run on connections 2024-02-04 13:50:15 +01:00
JKorf d0fc67355d wip 2024-02-03 15:35:27 +01:00
JKorf 8a869e8e1d wip 2024-02-03 15:35:27 +01:00
JKorf acd9b0d533 wip + tests 2024-02-03 15:35:27 +01:00
JKorf fc6503035a Cleanup 2024-02-03 15:35:27 +01:00
JKorf e3207033c3 wip 2024-02-03 15:35:27 +01:00
JKorf b057974cd0 wip 2024-02-03 15:35:27 +01:00
JKorf 9ead87d350 wip 2024-02-03 15:35:27 +01:00
JKorf c1ee36dd8a wip 2024-02-03 15:35:27 +01:00
JKorf eee19b28a5 wip 2024-02-03 15:35:27 +01:00
JKorf 58098edaa6 wip 2024-02-03 15:35:27 +01:00
JKorf c931a60cb7 wip 2024-02-03 15:35:27 +01:00
JKorf 12d5783625 wip 2024-02-03 15:35:27 +01:00
JKorf b640690a0f wip 2024-02-03 15:35:27 +01:00
JKorf c41e128900 wip 2024-02-03 15:35:27 +01:00
JKorf 1ba66be29f wip 2024-02-03 15:35:27 +01:00
JKorf ff6a9d5f13 wip 2024-02-03 15:35:26 +01:00
JKorf b59fe9e3ef wip 2024-02-03 15:35:26 +01:00
JKorf ac434fa2c6 wip 2024-02-03 15:35:26 +01:00
JKorf 3de04e4828 wip 2024-02-03 15:35:26 +01:00
JKorf 081c2d4268 wip 2024-02-03 15:35:26 +01:00
JKorf 6fa66d819d wip 2024-02-03 15:35:26 +01:00
JKorf 312d54cf04 wip 2024-02-03 15:35:26 +01:00
JKorf 35f7dbf9fb wip 2024-02-03 15:35:26 +01:00
JKorf 5539320827 wip 2024-02-03 15:35:26 +01:00
JKorf cf941fe5c9 wip 2024-02-03 15:35:26 +01:00
JKorf ad3959a8e9 wip 2024-02-03 15:35:26 +01:00
JKorf 9f92d86855 wip 2024-02-03 15:35:26 +01:00
JKorf bee2e86c2f wip 2024-02-03 15:35:26 +01:00
JKorf bf854c92af wip 2024-02-03 15:35:26 +01:00
JKorf 141d5bd956 wip 2024-02-03 15:35:26 +01:00
JKorf cff3863373 wip 2024-02-03 15:35:26 +01:00
591 changed files with 1375 additions and 117041 deletions
+2
View File
@@ -287,3 +287,5 @@ __pycache__/
*.odx.cs
*.xsd.cs
CryptoExchange.Net/CryptoExchange.Net.xml
/Docs/*
Docs/
@@ -1,6 +1,5 @@
using CryptoExchange.Net.Objects;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -25,8 +24,8 @@ namespace CryptoExchange.Net.UnitTests
var result1 = await waiter1;
var result2 = await waiter2;
Assert.That(result1);
Assert.That(result2);
Assert.True(result1);
Assert.True(result2);
}
[Test]
@@ -40,8 +39,8 @@ namespace CryptoExchange.Net.UnitTests
var result1 = await waiter1;
var result2 = await waiter2;
Assert.That(result1);
Assert.That(result2);
Assert.True(result1);
Assert.True(result2);
}
[Test]
@@ -56,14 +55,14 @@ namespace CryptoExchange.Net.UnitTests
var result1 = await waiter1;
Assert.That(result1);
Assert.That(waiter2.Status != TaskStatus.RanToCompletion);
Assert.True(result1);
Assert.True(waiter2.Status != TaskStatus.RanToCompletion);
evnt.Set();
var result2 = await waiter2;
Assert.That(result2);
Assert.True(result2);
}
[Test]
@@ -76,13 +75,13 @@ namespace CryptoExchange.Net.UnitTests
var result1 = await waiter1;
Assert.That(result1);
Assert.That(waiter2.Status != TaskStatus.RanToCompletion);
Assert.True(result1);
Assert.True(waiter2.Status != TaskStatus.RanToCompletion);
evnt.Set();
var result2 = await waiter2;
Assert.That(result2);
Assert.True(result2);
}
[Test]
@@ -106,12 +105,12 @@ namespace CryptoExchange.Net.UnitTests
for(var i = 1; i <= 10; i++)
{
evnt.Set();
Assert.That(10 - i == waiters.Count(w => w.Status != TaskStatus.RanToCompletion));
Assert.AreEqual(10 - i, waiters.Count(w => w.Status != TaskStatus.RanToCompletion));
}
await resultsWaiter;
Assert.That(10 == results.Count(r => r));
Assert.AreEqual(10, results.Count(r => r));
}
[Test]
@@ -125,7 +124,7 @@ namespace CryptoExchange.Net.UnitTests
var result1 = await waiter1;
Assert.That(result1);
Assert.True(result1);
}
[Test]
@@ -135,9 +134,9 @@ namespace CryptoExchange.Net.UnitTests
var waiter1 = evnt.WaitAsync(TimeSpan.FromMilliseconds(100));
var result1 = await waiter1;
var result1 = await waiter1;
ClassicAssert.False(result1);
Assert.False(result1);
}
}
}
@@ -3,7 +3,6 @@ using CryptoExchange.Net.Objects;
using CryptoExchange.Net.UnitTests.TestImplementations;
using Microsoft.Extensions.Logging;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using System;
using System.Collections.Generic;
@@ -22,7 +21,7 @@ namespace CryptoExchange.Net.UnitTests
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123}");
// assert
Assert.That(result.Success);
Assert.IsTrue(result.Success);
}
[TestCase]
@@ -35,8 +34,8 @@ namespace CryptoExchange.Net.UnitTests
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123");
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.IsFalse(result.Success);
Assert.IsTrue(result.Error != null);
}
[TestCase("https://api.test.com/api", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")]
@@ -49,7 +48,7 @@ namespace CryptoExchange.Net.UnitTests
public void AppendPathTests(string baseUrl, string[] path, string expected)
{
var result = baseUrl.AppendPath(path);
Assert.That(expected == result);
Assert.AreEqual(expected, result);
}
}
}
+51 -52
View File
@@ -1,6 +1,5 @@
using CryptoExchange.Net.Objects;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -18,9 +17,9 @@ namespace CryptoExchange.Net.UnitTests
{
var result = new CallResult(new ServerError("TestError"));
ClassicAssert.AreSame(result.Error.Message, "TestError");
ClassicAssert.IsFalse(result);
ClassicAssert.IsFalse(result.Success);
Assert.AreEqual(result.Error.Message, "TestError");
Assert.IsFalse(result);
Assert.IsFalse(result.Success);
}
[Test]
@@ -28,9 +27,9 @@ namespace CryptoExchange.Net.UnitTests
{
var result = new CallResult(null);
ClassicAssert.IsNull(result.Error);
Assert.That(result);
Assert.That(result.Success);
Assert.IsNull(result.Error);
Assert.IsTrue(result);
Assert.IsTrue(result.Success);
}
[Test]
@@ -38,10 +37,10 @@ namespace CryptoExchange.Net.UnitTests
{
var result = new CallResult<object>(new ServerError("TestError"));
ClassicAssert.AreSame(result.Error.Message, "TestError");
ClassicAssert.IsNull(result.Data);
ClassicAssert.IsFalse(result);
ClassicAssert.IsFalse(result.Success);
Assert.AreEqual(result.Error.Message, "TestError");
Assert.IsNull(result.Data);
Assert.IsFalse(result);
Assert.IsFalse(result.Success);
}
[Test]
@@ -49,10 +48,10 @@ namespace CryptoExchange.Net.UnitTests
{
var result = new CallResult<object>(new object());
ClassicAssert.IsNull(result.Error);
ClassicAssert.IsNotNull(result.Data);
Assert.That(result);
Assert.That(result.Success);
Assert.IsNull(result.Error);
Assert.IsNotNull(result.Data);
Assert.IsTrue(result);
Assert.IsTrue(result.Success);
}
[Test]
@@ -61,11 +60,11 @@ namespace CryptoExchange.Net.UnitTests
var result = new CallResult<TestObjectResult>(new TestObjectResult());
var asResult = result.As<TestObject2>(result.Data.InnerData);
ClassicAssert.IsNull(asResult.Error);
ClassicAssert.IsNotNull(asResult.Data);
Assert.That(asResult.Data is not null);
Assert.That(asResult);
Assert.That(asResult.Success);
Assert.IsNull(asResult.Error);
Assert.IsNotNull(asResult.Data);
Assert.IsTrue(asResult.Data is TestObject2);
Assert.IsTrue(asResult);
Assert.IsTrue(asResult.Success);
}
[Test]
@@ -74,11 +73,11 @@ namespace CryptoExchange.Net.UnitTests
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
var asResult = result.As<TestObject2>(default);
ClassicAssert.IsNotNull(asResult.Error);
ClassicAssert.AreSame(asResult.Error.Message, "TestError");
ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success);
Assert.IsNotNull(asResult.Error);
Assert.AreEqual(asResult.Error.Message, "TestError");
Assert.IsNull(asResult.Data);
Assert.IsFalse(asResult);
Assert.IsFalse(asResult.Success);
}
[Test]
@@ -87,11 +86,11 @@ namespace CryptoExchange.Net.UnitTests
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
ClassicAssert.IsNotNull(asResult.Error);
ClassicAssert.AreSame(asResult.Error.Message, "TestError2");
ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success);
Assert.IsNotNull(asResult.Error);
Assert.AreEqual(asResult.Error.Message, "TestError2");
Assert.IsNull(asResult.Data);
Assert.IsFalse(asResult);
Assert.IsFalse(asResult.Success);
}
[Test]
@@ -100,11 +99,11 @@ namespace CryptoExchange.Net.UnitTests
var result = new WebCallResult<TestObjectResult>(new ServerError("TestError"));
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
ClassicAssert.IsNotNull(asResult.Error);
ClassicAssert.AreSame(asResult.Error.Message, "TestError2");
ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success);
Assert.IsNotNull(asResult.Error);
Assert.AreEqual(asResult.Error.Message, "TestError2");
Assert.IsNull(asResult.Data);
Assert.IsFalse(asResult);
Assert.IsFalse(asResult.Success);
}
[Test]
@@ -125,15 +124,15 @@ namespace CryptoExchange.Net.UnitTests
null);
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
ClassicAssert.IsNotNull(asResult.Error);
Assert.That(asResult.Error.Message == "TestError2");
Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK);
Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1));
Assert.That(asResult.RequestUrl == "https://test.com/api");
Assert.That(asResult.RequestMethod == HttpMethod.Get);
ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success);
Assert.IsNotNull(asResult.Error);
Assert.AreEqual(asResult.Error.Message, "TestError2");
Assert.AreEqual(asResult.ResponseStatusCode, System.Net.HttpStatusCode.OK);
Assert.AreEqual(asResult.ResponseTime, TimeSpan.FromSeconds(1));
Assert.AreEqual(asResult.RequestUrl, "https://test.com/api");
Assert.AreEqual(asResult.RequestMethod, HttpMethod.Get);
Assert.IsNull(asResult.Data);
Assert.IsFalse(asResult);
Assert.IsFalse(asResult.Success);
}
[Test]
@@ -154,14 +153,14 @@ namespace CryptoExchange.Net.UnitTests
null);
var asResult = result.As<TestObject2>(result.Data.InnerData);
ClassicAssert.IsNull(asResult.Error);
Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK);
Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1));
Assert.That(asResult.RequestUrl == "https://test.com/api");
Assert.That(asResult.RequestMethod == HttpMethod.Get);
ClassicAssert.IsNotNull(asResult.Data);
Assert.That(asResult);
Assert.That(asResult.Success);
Assert.IsNull(asResult.Error);
Assert.AreEqual(asResult.ResponseStatusCode, System.Net.HttpStatusCode.OK);
Assert.AreEqual(asResult.ResponseTime, TimeSpan.FromSeconds(1));
Assert.AreEqual(asResult.RequestUrl, "https://test.com/api");
Assert.AreEqual(asResult.RequestMethod, HttpMethod.Get);
Assert.IsNotNull(asResult.Data);
Assert.IsTrue(asResult);
Assert.IsTrue(asResult.Success);
}
}
@@ -1,9 +1,7 @@
using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.Converters.JsonNet;
using Newtonsoft.Json;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -13,7 +11,7 @@ using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
public class JsonNetConverterTests
public class ConverterTests
{
[TestCase("2021-05-12")]
[TestCase("20210512")]
@@ -29,7 +27,7 @@ namespace CryptoExchange.Net.UnitTests
public void TestDateTimeConverterString(string input, bool expectNull = false)
{
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": \"{input}\" }}");
Assert.That(output.Time == (expectNull ? null: new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
Assert.AreEqual(output.Time, expectNull ? null: new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[TestCase(1620777600.000)]
@@ -37,7 +35,7 @@ namespace CryptoExchange.Net.UnitTests
public void TestDateTimeConverterDouble(double input)
{
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": {input} }}");
Assert.That(output.Time == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.AreEqual(output.Time, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[TestCase(1620777600)]
@@ -48,7 +46,7 @@ namespace CryptoExchange.Net.UnitTests
public void TestDateTimeConverterLong(long input, bool expectNull = false)
{
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": {input} }}");
Assert.That(output.Time == (expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
Assert.AreEqual(output.Time, expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[TestCase(1620777600)]
@@ -56,14 +54,14 @@ namespace CryptoExchange.Net.UnitTests
public void TestDateTimeConverterFromSeconds(double input)
{
var output = DateTimeConverter.ConvertFromSeconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.AreEqual(output, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToSeconds()
{
var output = DateTimeConverter.ConvertToSeconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600);
Assert.AreEqual(output, 1620777600);
}
[TestCase(1620777600000)]
@@ -71,49 +69,49 @@ namespace CryptoExchange.Net.UnitTests
public void TestDateTimeConverterFromMilliseconds(double input)
{
var output = DateTimeConverter.ConvertFromMilliseconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.AreEqual(output, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToMilliseconds()
{
var output = DateTimeConverter.ConvertToMilliseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600000);
Assert.AreEqual(output, 1620777600000);
}
[TestCase(1620777600000000)]
public void TestDateTimeConverterFromMicroseconds(long input)
{
var output = DateTimeConverter.ConvertFromMicroseconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.AreEqual(output, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToMicroseconds()
{
var output = DateTimeConverter.ConvertToMicroseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600000000);
Assert.AreEqual(output, 1620777600000000);
}
[TestCase(1620777600000000000)]
public void TestDateTimeConverterFromNanoseconds(long input)
{
var output = DateTimeConverter.ConvertFromNanoseconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.AreEqual(output, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToNanoseconds()
{
var output = DateTimeConverter.ConvertToNanoseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600000000000);
Assert.AreEqual(output, 1620777600000000000);
}
[TestCase()]
public void TestDateTimeConverterNull()
{
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": null }}");
Assert.That(output.Time == null);
Assert.AreEqual(output.Time, null);
}
[TestCase(TestEnum.One, "1")]
@@ -124,7 +122,7 @@ namespace CryptoExchange.Net.UnitTests
public void TestEnumConverterNullableGetStringTests(TestEnum? value, string expected)
{
var output = EnumConverter.GetString(value);
Assert.That(output == expected);
Assert.AreEqual(output, expected);
}
[TestCase(TestEnum.One, "1")]
@@ -134,7 +132,7 @@ namespace CryptoExchange.Net.UnitTests
public void TestEnumConverterGetStringTests(TestEnum value, string expected)
{
var output = EnumConverter.GetString(value);
Assert.That(output == expected);
Assert.AreEqual(output, expected);
}
[TestCase("1", TestEnum.One)]
@@ -149,7 +147,7 @@ namespace CryptoExchange.Net.UnitTests
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonConvert.DeserializeObject<EnumObject>($"{{ \"Value\": {val} }}");
Assert.That(output.Value == expected);
Assert.AreEqual(output.Value, expected);
}
[TestCase("1", TestEnum.One)]
@@ -164,7 +162,7 @@ namespace CryptoExchange.Net.UnitTests
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonConvert.DeserializeObject<NotNullableEnumObject>($"{{ \"Value\": {val} }}");
Assert.That(output.Value == expected);
Assert.AreEqual(output.Value, expected);
}
[TestCase("1", true)]
@@ -183,7 +181,7 @@ namespace CryptoExchange.Net.UnitTests
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonConvert.DeserializeObject<BoolObject>($"{{ \"Value\": {val} }}");
Assert.That(output.Value == expected);
Assert.AreEqual(output.Value, expected);
}
[TestCase("1", true)]
@@ -202,7 +200,7 @@ namespace CryptoExchange.Net.UnitTests
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonConvert.DeserializeObject<NotNullableBoolObject>($"{{ \"Value\": {val} }}");
Assert.That(output.Value == expected);
Assert.AreEqual(output.Value, expected);
}
}
@@ -6,10 +6,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0"></PackageReference>
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="NUnit" Version="4.1.0"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0"></PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0-preview-20211130-02"></PackageReference>
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="NUnit" Version="3.13.2"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="4.2.0"></PackageReference>
</ItemGroup>
<ItemGroup>
@@ -1,6 +1,5 @@
using CryptoExchange.Net.Objects;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using System.Globalization;
namespace CryptoExchange.Net.UnitTests
@@ -17,7 +16,7 @@ namespace CryptoExchange.Net.UnitTests
public void ClampValueTests(decimal min, decimal max, decimal input, decimal expected)
{
var result = ExchangeHelpers.ClampValue(min, max, input);
Assert.That(expected == result);
Assert.AreEqual(expected, result);
}
[TestCase(0.1, 1, 0.1, RoundingType.Down, 0.4, 0.4)]
@@ -34,7 +33,7 @@ namespace CryptoExchange.Net.UnitTests
public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected)
{
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
Assert.That(expected == result);
Assert.AreEqual(expected, result);
}
[TestCase(0.1, 1, 2, RoundingType.Closest, 0.4, 0.4)]
@@ -49,7 +48,7 @@ namespace CryptoExchange.Net.UnitTests
public void AdjustValuePrecisionTests(decimal min, decimal max, int? precision, RoundingType roundingType, decimal input, decimal expected)
{
var result = ExchangeHelpers.AdjustValuePrecision(min, max, precision, roundingType, input);
Assert.That(expected == result);
Assert.AreEqual(expected, result);
}
[TestCase(5, 0.1563158, 0.15631)]
@@ -60,7 +59,7 @@ namespace CryptoExchange.Net.UnitTests
public void RoundDownTests(int decimalPlaces, decimal input, decimal expected)
{
var result = ExchangeHelpers.RoundDown(input, decimalPlaces);
Assert.That(expected == result);
Assert.AreEqual(expected, result);
}
[TestCase(0.1234560000, "0.123456")]
@@ -68,7 +67,7 @@ namespace CryptoExchange.Net.UnitTests
public void NormalizeTests(decimal input, string expected)
{
var result = ExchangeHelpers.Normalize(input);
Assert.That(expected == result.ToString(CultureInfo.InvariantCulture));
Assert.AreEqual(expected, result.ToString(CultureInfo.InvariantCulture));
}
}
}
+20 -21
View File
@@ -4,7 +4,6 @@ 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;
@@ -50,9 +49,9 @@ namespace CryptoExchange.Net.UnitTests
};
// assert
Assert.That(options.ReceiveWindow == TimeSpan.FromSeconds(10));
Assert.That(options.ApiCredentials.Key.GetString() == "123");
Assert.That(options.ApiCredentials.Secret.GetString() == "456");
Assert.AreEqual(options.ReceiveWindow, TimeSpan.FromSeconds(10));
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
}
[Test]
@@ -64,10 +63,10 @@ namespace CryptoExchange.Net.UnitTests
options.Api2Options.ApiCredentials = new ApiCredentials("789", "101");
// assert
Assert.That(options.Api1Options.ApiCredentials.Key.GetString() == "123");
Assert.That(options.Api1Options.ApiCredentials.Secret.GetString() == "456");
Assert.That(options.Api2Options.ApiCredentials.Key.GetString() == "789");
Assert.That(options.Api2Options.ApiCredentials.Secret.GetString() == "101");
Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "123");
Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "456");
Assert.AreEqual(options.Api2Options.ApiCredentials.Key.GetString(), "789");
Assert.AreEqual(options.Api2Options.ApiCredentials.Secret.GetString(), "101");
}
[Test]
@@ -80,10 +79,10 @@ namespace CryptoExchange.Net.UnitTests
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
Assert.That(authProvider1.GetKey() == "111");
Assert.That(authProvider1.GetSecret() == "222");
Assert.That(authProvider2.GetKey() == "333");
Assert.That(authProvider2.GetSecret() == "444");
Assert.AreEqual(authProvider1.GetKey(), "111");
Assert.AreEqual(authProvider1.GetSecret(), "222");
Assert.AreEqual(authProvider2.GetKey(), "333");
Assert.AreEqual(authProvider2.GetSecret(), "444");
}
[Test]
@@ -96,10 +95,10 @@ namespace CryptoExchange.Net.UnitTests
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
Assert.That(authProvider1.GetKey() == "111");
Assert.That(authProvider1.GetSecret() == "222");
Assert.That(authProvider2.GetKey() == "123");
Assert.That(authProvider2.GetSecret() == "456");
Assert.AreEqual(authProvider1.GetKey(), "111");
Assert.AreEqual(authProvider1.GetSecret(), "222");
Assert.AreEqual(authProvider2.GetKey(), "123");
Assert.AreEqual(authProvider2.GetSecret(), "456");
}
[Test]
@@ -116,11 +115,11 @@ namespace CryptoExchange.Net.UnitTests
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
Assert.That(authProvider1.GetKey() == "333");
Assert.That(authProvider1.GetSecret() == "444");
Assert.That(authProvider2.GetKey() == "123");
Assert.That(authProvider2.GetSecret() == "456");
Assert.That(client.Api2.BaseAddress == "https://localhost:123");
Assert.AreEqual(authProvider1.GetKey(), "333");
Assert.AreEqual(authProvider1.GetSecret(), "444");
Assert.AreEqual(authProvider2.GetKey(), "123");
Assert.AreEqual(authProvider2.GetSecret(), "456");
Assert.AreEqual(client.Api2.BaseAddress, "https://localhost:123");
}
}
+41 -42
View File
@@ -12,7 +12,6 @@ using Microsoft.Extensions.Logging;
using System.Net.Http;
using System.Threading.Tasks;
using System.Threading;
using NUnit.Framework.Legacy;
namespace CryptoExchange.Net.UnitTests
{
@@ -31,8 +30,8 @@ namespace CryptoExchange.Net.UnitTests
var result = client.Api1.Request<TestObject>().Result;
// assert
Assert.That(result.Success);
Assert.That(TestHelpers.AreEqual(expected, result.Data));
Assert.IsTrue(result.Success);
Assert.IsTrue(TestHelpers.AreEqual(expected, result.Data));
}
[TestCase]
@@ -46,8 +45,8 @@ namespace CryptoExchange.Net.UnitTests
var result = client.Api1.Request<TestObject>().Result;
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.IsFalse(result.Success);
Assert.IsTrue(result.Error != null);
}
[TestCase]
@@ -61,8 +60,8 @@ namespace CryptoExchange.Net.UnitTests
var result = await client.Api1.Request<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.IsFalse(result.Success);
Assert.IsTrue(result.Error != null);
}
[TestCase]
@@ -76,11 +75,11 @@ namespace CryptoExchange.Net.UnitTests
var result = await client.Api1.Request<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.That(result.Error is ServerError);
Assert.That(result.Error.Message.Contains("Invalid request"));
Assert.That(result.Error.Message.Contains("123"));
Assert.IsFalse(result.Success);
Assert.IsTrue(result.Error != null);
Assert.IsTrue(result.Error is ServerError);
Assert.IsTrue(result.Error.Message.Contains("Invalid request"));
Assert.IsTrue(result.Error.Message.Contains("123"));
}
[TestCase]
@@ -94,11 +93,11 @@ namespace CryptoExchange.Net.UnitTests
var result = await client.Api2.Request<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.That(result.Error is ServerError);
Assert.That(result.Error.Code == 123);
Assert.That(result.Error.Message == "Invalid request");
Assert.IsFalse(result.Success);
Assert.IsTrue(result.Error != null);
Assert.IsTrue(result.Error is ServerError);
Assert.IsTrue(result.Error.Code == 123);
Assert.IsTrue(result.Error.Message == "Invalid request");
}
[TestCase]
@@ -113,9 +112,9 @@ namespace CryptoExchange.Net.UnitTests
var client = new TestBaseClient(options);
// assert
Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.RateLimiters.Count == 1);
Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.RateLimitingBehaviour == RateLimitingBehaviour.Fail);
Assert.That(((TestClientOptions)client.ClientOptions).RequestTimeout == TimeSpan.FromMinutes(1));
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimiters.Count == 1);
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimitingBehaviour == RateLimitingBehaviour.Fail);
Assert.IsTrue(((TestClientOptions)client.ClientOptions).RequestTimeout == TimeSpan.FromMinutes(1));
}
[TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
@@ -146,13 +145,13 @@ namespace CryptoExchange.Net.UnitTests
});
// assert
Assert.That(request.Method == new HttpMethod(method));
Assert.That((request.Content?.Contains("TestParam1") == true) == (pos == HttpMethodParameterPosition.InBody));
Assert.That((request.Uri.ToString().Contains("TestParam1")) == (pos == HttpMethodParameterPosition.InUri));
Assert.That((request.Content?.Contains("TestParam2") == true) == (pos == HttpMethodParameterPosition.InBody));
Assert.That((request.Uri.ToString().Contains("TestParam2")) == (pos == HttpMethodParameterPosition.InUri));
Assert.That(request.GetHeaders().First().Key == "TestHeader");
Assert.That(request.GetHeaders().First().Value.Contains("123"));
Assert.AreEqual(request.Method, new HttpMethod(method));
Assert.AreEqual(request.Content?.Contains("TestParam1") == true, pos == HttpMethodParameterPosition.InBody);
Assert.AreEqual(request.Uri.ToString().Contains("TestParam1"), pos == HttpMethodParameterPosition.InUri);
Assert.AreEqual(request.Content?.Contains("TestParam2") == true, pos == HttpMethodParameterPosition.InBody);
Assert.AreEqual(request.Uri.ToString().Contains("TestParam2"), pos == HttpMethodParameterPosition.InUri);
Assert.AreEqual(request.GetHeaders().First().Key, "TestHeader");
Assert.IsTrue(request.GetHeaders().First().Value.Contains("123"));
}
@@ -168,12 +167,12 @@ namespace CryptoExchange.Net.UnitTests
for (var i = 0; i < requests + 1; i++)
{
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.That(i == requests? result1.Data > 1 : result1.Data == 0);
Assert.IsTrue(i == requests? result1.Data > 1 : result1.Data == 0);
}
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.That(result2.Data == 0);
Assert.IsTrue(result2.Data == 0);
}
[TestCase("/sapi/test1", true)]
@@ -190,7 +189,7 @@ namespace CryptoExchange.Net.UnitTests
{
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
bool expected = i == 1 ? (expectLimiting ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
Assert.That(expected);
Assert.IsTrue(expected);
}
}
[TestCase("/sapi/", "/sapi/", true)]
@@ -204,8 +203,8 @@ namespace CryptoExchange.Net.UnitTests
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.That(result1.Data == 0);
Assert.That(expectLimiting ? result2.Data > 0 : result2.Data == 0);
Assert.IsTrue(result1.Data == 0);
Assert.IsTrue(expectLimiting ? result2.Data > 0 : result2.Data == 0);
}
[TestCase(1, 0.1)]
@@ -220,12 +219,12 @@ namespace CryptoExchange.Net.UnitTests
for (var i = 0; i < requests + 1; i++)
{
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.That(i == requests ? result1.Data > 1 : result1.Data == 0);
Assert.IsTrue(i == requests ? result1.Data > 1 : result1.Data == 0);
}
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.That(result2.Data == 0);
Assert.IsTrue(result2.Data == 0);
}
[TestCase("/", false)]
@@ -240,7 +239,7 @@ namespace CryptoExchange.Net.UnitTests
{
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
Assert.That(expected);
Assert.IsTrue(expected);
}
}
@@ -257,7 +256,7 @@ namespace CryptoExchange.Net.UnitTests
{
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
Assert.That(expected);
Assert.IsTrue(expected);
}
}
@@ -289,8 +288,8 @@ namespace CryptoExchange.Net.UnitTests
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, signed1, key1?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, signed2, key2?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.That(result1.Data == 0);
Assert.That(expectLimited ? result2.Data > 0 : result2.Data == 0);
Assert.IsTrue(result1.Data == 0);
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
}
[TestCase("/sapi/test", "/sapi/test", true)]
@@ -303,8 +302,8 @@ namespace CryptoExchange.Net.UnitTests
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, true, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.That(result1.Data == 0);
Assert.That(expectLimited ? result2.Data > 0 : result2.Data == 0);
Assert.IsTrue(result1.Data == 0);
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
}
[TestCase("/sapi/test", true, true, true, false)]
@@ -319,8 +318,8 @@ namespace CryptoExchange.Net.UnitTests
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, signed1, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, signed2, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.That(result1.Data == 0);
Assert.That(expectLimited ? result2.Data > 0 : result2.Data == 0);
Assert.IsTrue(result1.Data == 0);
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
}
}
}
@@ -9,9 +9,9 @@ using CryptoExchange.Net.UnitTests.TestImplementations;
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
using Microsoft.Extensions.Logging;
using Moq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using NUnit.Framework.Constraints;
namespace CryptoExchange.Net.UnitTests
{
@@ -29,9 +29,10 @@ namespace CryptoExchange.Net.UnitTests
options.SubOptions.MaxSocketConnections = 1;
});
//assert
ClassicAssert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
Assert.That(1 == client.SubClient.ApiOptions.MaxSocketConnections);
Assert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
Assert.AreEqual(1, client.SubClient.ApiOptions.MaxSocketConnections);
}
[TestCase(true)]
@@ -47,11 +48,11 @@ namespace CryptoExchange.Net.UnitTests
var connectResult = client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, null));
//assert
Assert.That(connectResult.Success == canConnect);
Assert.IsTrue(connectResult.Success == canConnect);
}
[TestCase]
public void SocketMessages_Should_BeProcessedInDataHandlers()
public async Task SocketMessages_Should_BeProcessedInDataHandlers()
{
// arrange
var client = new TestSocketClient(options => {
@@ -67,25 +68,23 @@ namespace CryptoExchange.Net.UnitTests
client.SubClient.ConnectSocketSub(sub);
var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
sub.AddSubscription(new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
{
result = messageEvent.Data;
rstEvent.Set();
});
subObj.HandleUpdatesBeforeConfirmation = true;
sub.AddSubscription(subObj);
}));
// act
socket.InvokeMessage("{\"property\": \"123\", \"topic\": \"topic\"}");
await socket.InvokeMessage("{\"property\": \"123\", \"topic\": \"topic\"}");
rstEvent.WaitOne(1000);
// assert
Assert.That(result["property"] == "123");
Assert.IsTrue(result["property"] == "123");
}
[TestCase(false)]
[TestCase(true)]
public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
public async Task SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
{
// arrange
var client = new TestSocketClient(options =>
@@ -102,21 +101,18 @@ namespace CryptoExchange.Net.UnitTests
string original = null;
client.SubClient.ConnectSocketSub(sub);
var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
sub.AddSubscription(new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
{
original = messageEvent.OriginalData;
rstEvent.Set();
});
subObj.HandleUpdatesBeforeConfirmation = true;
sub.AddSubscription(subObj);
var msgToSend = JsonConvert.SerializeObject(new { topic = "topic", property = 123 });
}));
// act
socket.InvokeMessage(msgToSend);
await socket.InvokeMessage("{\"property\": 123}");
rstEvent.WaitOne(1000);
// assert
Assert.That(original == (enabled ? msgToSend : null));
Assert.IsTrue(original == (enabled ? "{\"property\": 123}" : null));
}
[TestCase()]
@@ -140,7 +136,7 @@ namespace CryptoExchange.Net.UnitTests
client.UnsubscribeAsync(ups).Wait();
// assert
Assert.That(socket.Connected == false);
Assert.IsTrue(socket.Connected == false);
}
[TestCase()]
@@ -168,8 +164,8 @@ namespace CryptoExchange.Net.UnitTests
client.UnsubscribeAllAsync().Wait();
// assert
Assert.That(socket1.Connected == false);
Assert.That(socket2.Connected == false);
Assert.IsTrue(socket1.Connected == false);
Assert.IsTrue(socket2.Connected == false);
}
[TestCase()]
@@ -185,53 +181,7 @@ namespace CryptoExchange.Net.UnitTests
var connectResult = client.SubClient.ConnectSocketSub(sub1);
// 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"));
// act
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, status = "error" }));
await sub;
// assert
ClassicAssert.IsFalse(client.SubClient.TestSubscription.Confirmed);
}
[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"));
// act
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, status = "confirmed" }));
await sub;
// assert
Assert.That(client.SubClient.TestSubscription.Confirmed);
Assert.IsFalse(connectResult.Success);
}
}
}
@@ -8,7 +8,6 @@ using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.OrderBook;
using NUnit.Framework;
using NUnit.Framework.Legacy;
namespace CryptoExchange.Net.UnitTests
{
@@ -19,7 +18,7 @@ namespace CryptoExchange.Net.UnitTests
private class TestableSymbolOrderBook : SymbolOrderBook
{
public TestableSymbolOrderBook() : base(null, "Test", "Test", "BTC/USD")
public TestableSymbolOrderBook() : base(null, "Test", "BTC/USD")
{
Initialize(_defaultOrderBookOptions);
}
@@ -57,31 +56,31 @@ namespace CryptoExchange.Net.UnitTests
public void GivenEmptyBidList_WhenBestBid_ThenEmptySymbolOrderBookEntry()
{
var symbolOrderBook = new TestableSymbolOrderBook();
ClassicAssert.IsNotNull(symbolOrderBook.BestBid);
Assert.That(0m == symbolOrderBook.BestBid.Price);
Assert.That(0m == symbolOrderBook.BestAsk.Quantity);
Assert.IsNotNull(symbolOrderBook.BestBid);
Assert.AreEqual(0m, symbolOrderBook.BestBid.Price);
Assert.AreEqual(0m, symbolOrderBook.BestAsk.Quantity);
}
[TestCase]
public void GivenEmptyAskList_WhenBestAsk_ThenEmptySymbolOrderBookEntry()
{
var symbolOrderBook = new TestableSymbolOrderBook();
ClassicAssert.IsNotNull(symbolOrderBook.BestBid);
Assert.That(0m == symbolOrderBook.BestBid.Price);
Assert.That(0m == symbolOrderBook.BestAsk.Quantity);
Assert.IsNotNull(symbolOrderBook.BestBid);
Assert.AreEqual(0m, symbolOrderBook.BestBid.Price);
Assert.AreEqual(0m, symbolOrderBook.BestAsk.Quantity);
}
[TestCase]
public void GivenEmptyBidAndAskList_WhenBestOffers_ThenEmptySymbolOrderBookEntries()
{
var symbolOrderBook = new TestableSymbolOrderBook();
ClassicAssert.IsNotNull(symbolOrderBook.BestOffers);
ClassicAssert.IsNotNull(symbolOrderBook.BestOffers.Bid);
ClassicAssert.IsNotNull(symbolOrderBook.BestOffers.Ask);
Assert.That(0m == symbolOrderBook.BestOffers.Bid.Price);
Assert.That(0m == symbolOrderBook.BestOffers.Bid.Quantity);
Assert.That(0m == symbolOrderBook.BestOffers.Ask.Price);
Assert.That(0m == symbolOrderBook.BestOffers.Ask.Quantity);
Assert.IsNotNull(symbolOrderBook.BestOffers);
Assert.IsNotNull(symbolOrderBook.BestOffers.Bid);
Assert.IsNotNull(symbolOrderBook.BestOffers.Ask);
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Bid.Price);
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Bid.Quantity);
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Ask.Price);
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Ask.Quantity);
}
[TestCase]
@@ -104,12 +103,12 @@ namespace CryptoExchange.Net.UnitTests
var resultBids2 = orderbook.CalculateAverageFillPrice(1.5m, OrderBookEntryType.Bid);
var resultAsks2 = orderbook.CalculateAverageFillPrice(1.5m, OrderBookEntryType.Ask);
Assert.That(resultBids.Success);
Assert.That(resultAsks.Success);
Assert.That(1.05m == resultBids.Data);
Assert.That(1.25m == resultAsks.Data);
Assert.That(1.06666667m == resultBids2.Data);
Assert.That(1.23333333m == resultAsks2.Data);
Assert.True(resultBids.Success);
Assert.True(resultAsks.Success);
Assert.AreEqual(1.05m, resultBids.Data);
Assert.AreEqual(1.25m, resultAsks.Data);
Assert.AreEqual(1.06666667m, resultBids2.Data);
Assert.AreEqual(1.23333333m, resultAsks2.Data);
}
[TestCase]
@@ -132,12 +131,12 @@ namespace CryptoExchange.Net.UnitTests
var resultBids2 = orderbook.CalculateTradableAmount(1.5m, OrderBookEntryType.Bid);
var resultAsks2 = orderbook.CalculateTradableAmount(1.5m, OrderBookEntryType.Ask);
Assert.That(resultBids.Success);
Assert.That(resultAsks.Success);
Assert.That(1.9m == resultBids.Data);
Assert.That(1.61538462m == resultAsks.Data);
Assert.That(1.4m == resultBids2.Data);
Assert.That(1.23076923m == resultAsks2.Data);
Assert.True(resultBids.Success);
Assert.True(resultAsks.Success);
Assert.AreEqual(1.9m, resultBids.Data);
Assert.AreEqual(1.61538462m, resultAsks.Data);
Assert.AreEqual(1.4m, resultBids2.Data);
Assert.AreEqual(1.23076923m, resultAsks2.Data);
}
}
}
@@ -1,235 +0,0 @@
using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Converters.SystemTextJson;
using System.Text.Json;
using NUnit.Framework;
using System;
using System.Text.Json.Serialization;
using NUnit.Framework.Legacy;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
public class SystemTextJsonConverterTests
{
[TestCase("2021-05-12")]
[TestCase("20210512")]
[TestCase("210512")]
[TestCase("1620777600.000")]
[TestCase("1620777600000")]
[TestCase("2021-05-12T00:00:00.000Z")]
[TestCase("2021-05-12T00:00:00.000000000Z")]
[TestCase("0.000000", true)]
[TestCase("0", true)]
[TestCase("", true)]
[TestCase(" ", true)]
public void TestDateTimeConverterString(string input, bool expectNull = false)
{
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": \"{input}\" }}");
Assert.That(output.Time == (expectNull ? null: new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
}
[TestCase(1620777600.000)]
[TestCase(1620777600000d)]
public void TestDateTimeConverterDouble(double input)
{
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": {input} }}");
Assert.That(output.Time == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[TestCase(1620777600)]
[TestCase(1620777600000)]
[TestCase(1620777600000000)]
[TestCase(1620777600000000000)]
[TestCase(0, true)]
public void TestDateTimeConverterLong(long input, bool expectNull = false)
{
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": {input} }}");
Assert.That(output.Time == (expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
}
[TestCase(1620777600)]
[TestCase(1620777600.000)]
public void TestDateTimeConverterFromSeconds(double input)
{
var output = DateTimeConverter.ConvertFromSeconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToSeconds()
{
var output = DateTimeConverter.ConvertToSeconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600);
}
[TestCase(1620777600000)]
[TestCase(1620777600000.000)]
public void TestDateTimeConverterFromMilliseconds(double input)
{
var output = DateTimeConverter.ConvertFromMilliseconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToMilliseconds()
{
var output = DateTimeConverter.ConvertToMilliseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600000);
}
[TestCase(1620777600000000)]
public void TestDateTimeConverterFromMicroseconds(long input)
{
var output = DateTimeConverter.ConvertFromMicroseconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToMicroseconds()
{
var output = DateTimeConverter.ConvertToMicroseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600000000);
}
[TestCase(1620777600000000000)]
public void TestDateTimeConverterFromNanoseconds(long input)
{
var output = DateTimeConverter.ConvertFromNanoseconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToNanoseconds()
{
var output = DateTimeConverter.ConvertToNanoseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600000000000);
}
[TestCase()]
public void TestDateTimeConverterNull()
{
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": null }}");
Assert.That(output.Time == null);
}
[TestCase(TestEnum.One, "1")]
[TestCase(TestEnum.Two, "2")]
[TestCase(TestEnum.Three, "three")]
[TestCase(TestEnum.Four, "Four")]
[TestCase(null, null)]
public void TestEnumConverterNullableGetStringTests(TestEnum? value, string expected)
{
var output = EnumConverter.GetString(value);
Assert.That(output == expected);
}
[TestCase(TestEnum.One, "1")]
[TestCase(TestEnum.Two, "2")]
[TestCase(TestEnum.Three, "three")]
[TestCase(TestEnum.Four, "Four")]
public void TestEnumConverterGetStringTests(TestEnum value, string expected)
{
var output = EnumConverter.GetString(value);
Assert.That(output == expected);
}
[TestCase("1", TestEnum.One)]
[TestCase("2", TestEnum.Two)]
[TestCase("3", TestEnum.Three)]
[TestCase("three", TestEnum.Three)]
[TestCase("Four", TestEnum.Four)]
[TestCase("four", TestEnum.Four)]
[TestCase("Four1", null)]
[TestCase(null, null)]
public void TestEnumConverterNullableDeserializeTests(string value, TestEnum? expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<STJEnumObject>($"{{ \"Value\": {val} }}");
Assert.That(output.Value == expected);
}
[TestCase("1", TestEnum.One)]
[TestCase("2", TestEnum.Two)]
[TestCase("3", TestEnum.Three)]
[TestCase("three", TestEnum.Three)]
[TestCase("Four", TestEnum.Four)]
[TestCase("four", TestEnum.Four)]
[TestCase("Four1", TestEnum.One)]
[TestCase(null, TestEnum.One)]
public void TestEnumConverterNotNullableDeserializeTests(string value, TestEnum? expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<NotNullableSTJEnumObject>($"{{ \"Value\": {val} }}");
Assert.That(output.Value == expected);
}
[TestCase("1", true)]
[TestCase("true", true)]
[TestCase("yes", true)]
[TestCase("y", true)]
[TestCase("on", true)]
[TestCase("-1", false)]
[TestCase("0", false)]
[TestCase("n", false)]
[TestCase("no", false)]
[TestCase("false", false)]
[TestCase("off", false)]
[TestCase("", null)]
public void TestBoolConverter(string value, bool? expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<STJBoolObject>($"{{ \"Value\": {val} }}");
Assert.That(output.Value == expected);
}
[TestCase("1", true)]
[TestCase("true", true)]
[TestCase("yes", true)]
[TestCase("y", true)]
[TestCase("on", true)]
[TestCase("-1", false)]
[TestCase("0", false)]
[TestCase("n", false)]
[TestCase("no", false)]
[TestCase("false", false)]
[TestCase("off", false)]
[TestCase("", false)]
public void TestBoolConverterNotNullable(string value, bool expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}");
Assert.That(output.Value == expected);
}
}
public class STJTimeObject
{
[JsonConverter(typeof(DateTimeConverter))]
[JsonPropertyName("time")]
public DateTime? Time { get; set; }
}
public class STJEnumObject
{
[JsonConverter(typeof(EnumConverter))]
public TestEnum? Value { get; set; }
}
public class NotNullableSTJEnumObject
{
[JsonConverter(typeof(EnumConverter))]
public TestEnum Value { get; set; }
}
public class STJBoolObject
{
[JsonConverter(typeof(BoolConverter))]
public bool? Value { get; set; }
}
public class NotNullableSTJBoolObject
{
[JsonConverter(typeof(BoolConverter))]
public bool Value { get; set; }
}
}
@@ -1,45 +0,0 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{
internal class SubResponse
{
[JsonProperty("channel")]
public string Channel { get; set; } = null!;
[JsonProperty("status")]
public string Status { get; set; } = null!;
}
internal class UnsubResponse
{
[JsonProperty("status")]
public string Status { get; set; } = null!;
}
internal class TestChannelQuery : Query<SubResponse>
{
public override HashSet<string> ListenerIdentifiers { get; set; }
public TestChannelQuery(string channel, string request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
{
ListenerIdentifiers = new HashSet<string> { channel };
}
public override CallResult<SubResponse> HandleMessage(SocketConnection connection, DataEvent<SubResponse> message)
{
if (!message.Data.Status.Equals("confirmed", StringComparison.OrdinalIgnoreCase))
{
return new CallResult<SubResponse>(new ServerError(message.Data.Status));
}
return base.HandleMessage(connection, message);
}
}
}
@@ -1,7 +1,7 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
@@ -22,11 +22,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
_handler = handler;
}
public override CallResult DoHandleMessage(SocketConnection connection, DataEvent<object> message)
public override Task<CallResult> DoHandleMessageAsync(SocketConnection connection, DataEvent<object> message)
{
var data = (T)message.Data;
_handler.Invoke(message.As(data));
return new CallResult(null);
return Task.FromResult(new CallResult(null));
}
public override Type GetMessageType(IMessageAccessor message) => typeof(T);
@@ -1,38 +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 override HashSet<string> ListenerIdentifiers { get; set; }
public TestSubscriptionWithResponseCheck(string channel, Action<DataEvent<T>> handler) : base(Mock.Of<ILogger>(), false)
{
ListenerIdentifiers = new HashSet<string>() { channel };
_handler = handler;
_channel = channel;
}
public override CallResult DoHandleMessage(SocketConnection connection, DataEvent<object> message)
{
var data = (T)message.Data;
_handler.Invoke(message.As(data));
return new CallResult(null);
}
public override Type GetMessageType(IMessageAccessor message) => typeof(T);
public override Query GetSubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "subscribe", false, 1);
public override Query GetUnsubQuery() => new TestChannelQuery(_channel, "unsubscribe", false, 1);
}
}
@@ -1,11 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.UnitTests.TestImplementations;
@@ -42,17 +39,7 @@ namespace CryptoExchange.Net.UnitTests
{
}
public CallResult<T> Deserialize<T>(string data)
{
var stream = new MemoryStream(Encoding.UTF8.GetBytes(data));
var accessor = CreateAccessor();
var valid = accessor.Read(stream, true).Result;
if (!valid)
return new CallResult<T>(new ServerError(data));
var deserializeResult = accessor.Deserialize<T>();
return deserializeResult;
}
public CallResult<T> Deserialize<T>(string data) => Deserialize<T>(data, null, null);
public override TimeSpan? GetTimeOffset() => null;
public override TimeSyncInfo GetTimeSyncInfo() => null;
@@ -66,7 +53,7 @@ namespace CryptoExchange.Net.UnitTests
{
}
public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, Dictionary<string, object> providedParameters, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, RequestBodyFormat bodyFormat, out SortedDictionary<string, object> uriParameters, out SortedDictionary<string, object> bodyParameters, out Dictionary<string, string> headers)
public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, Dictionary<string, object> providedParameters, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, out SortedDictionary<string, object> uriParameters, out SortedDictionary<string, object> bodyParameters, out Dictionary<string, string> headers)
{
bodyParameters = new SortedDictionary<string, object>();
uriParameters = new SortedDictionary<string, object>();
@@ -14,7 +14,6 @@ using CryptoExchange.Net.Authentication;
using System.Collections.Generic;
using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging;
using CryptoExchange.Net.Clients;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
@@ -183,11 +182,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
}
protected override Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, IMessageAccessor accessor)
protected override Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, string data)
{
var errorData = accessor.Deserialize<TestError>();
var errorData = ValidateJson(data);
return new ServerError(errorData.Data.ErrorCode, errorData.Data.ErrorMessage);
return new ServerError((int)errorData.Data["errorCode"], (string)errorData.Data["errorMessage"]);
}
public override TimeSpan? GetTimeOffset()
@@ -209,12 +208,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
}
}
public class TestError
{
public int ErrorCode { get; set; }
public string ErrorMessage { get; set; }
}
public class ParseErrorTestRestClient: TestRestClient
{
public ParseErrorTestRestClient() { }
@@ -20,7 +20,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public event Func<Task> OnReconnecting;
#pragma warning restore 0067
public event Func<int, Task> OnRequestSent;
public event Action<WebSocketMessageType, ReadOnlyMemory<byte>> OnStreamMessage;
public event Func<WebSocketMessageType, Stream, Task> OnStreamMessage;
public event Func<Exception, Task> OnError;
public event Func<Task> OnOpen;
public Func<Task<Uri>> GetReconnectionUrl { get; set; }
@@ -111,9 +111,10 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
OnOpen?.Invoke();
}
public void InvokeMessage(string data)
public async Task InvokeMessage(string data)
{
OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(data)));
var stream = new MemoryStream(Encoding.UTF8.GetBytes(data));
await OnStreamMessage?.Invoke(WebSocketMessageType.Text, stream);
}
public void SetProxy(ApiProxy proxy)
@@ -1,16 +1,13 @@
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 CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
using Microsoft.Extensions.Logging;
using Moq;
using Newtonsoft.Json.Linq;
@@ -74,12 +71,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public class TestSubSocketClient : SocketApiClient
{
private MessagePath _channelPath = MessagePath.Get().Property("channel");
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)
public TestSubSocketClient(TestSocketOptions options, SocketApiOptions apiOptions): base(new TraceLogger(), options.Environment.TestAddress, options, apiOptions)
{
}
@@ -97,23 +90,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
return ConnectSocketAsync(sub).Result;
}
public override string GetListenerIdentifier(IMessageAccessor message)
{
if (!message.IsJson)
{
return "topic";
}
var id = message.GetValue<string>(_channelPath);
id ??= message.GetValue<string>(_topicPath);
return id;
}
public Task<CallResult<UpdateSubscription>> SubscribeToSomethingAsync(string channel, Action<DataEvent<string>> onUpdate, CancellationToken ct)
{
TestSubscription = new TestSubscriptionWithResponseCheck<string>(channel, onUpdate);
return SubscribeAsync(TestSubscription, ct);
}
public override string GetListenerIdentifier(IMessageAccessor messageAccessor) => "topic";
}
}
@@ -1,8 +1,8 @@
using System;
using System.IO;
using System.Security;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Converters.MessageParsing;
using System.Text;
using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net.Authentication
{
@@ -94,21 +94,38 @@ namespace CryptoExchange.Net.Authentication
/// <param name="identifierSecret">A key to identify the credentials for the API. For example, when set to `binanceSecret` the json data should contain a value for the property `binanceSecret`. Defaults to 'apiSecret'.</param>
public ApiCredentials(Stream inputStream, string? identifierKey = null, string? identifierSecret = null)
{
var accessor = new SystemTextJsonStreamMessageAccessor();
if (!accessor.Read(inputStream, false).Result)
using var reader = new StreamReader(inputStream, Encoding.UTF8, false, 512, true);
var stringData = reader.ReadToEnd();
var jsonData = stringData.ToJToken();
if(jsonData == null)
throw new ArgumentException("Input stream not valid json data");
var key = accessor.GetValue<string>(MessagePath.Get().Property(identifierKey ?? "apiKey"));
var secret = accessor.GetValue<string>(MessagePath.Get().Property(identifierSecret ?? "apiSecret"));
var key = TryGetValue(jsonData, identifierKey ?? "apiKey");
var secret = TryGetValue(jsonData, identifierSecret ?? "apiSecret");
if (key == null || secret == null)
throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");
Key = key.ToSecureString();
Secret = secret.ToSecureString();
Secret = secret.ToSecureString();
inputStream.Seek(0, SeekOrigin.Begin);
}
/// <summary>
/// Try get the value of a key from a JToken
/// </summary>
/// <param name="data"></param>
/// <param name="key"></param>
/// <returns></returns>
protected string? TryGetValue(JToken data, string key)
{
if (data[key] == null)
return null;
return (string) data[key]!;
}
/// <summary>
/// Dispose
/// </summary>
@@ -1,5 +1,4 @@
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic;
@@ -48,7 +47,6 @@ namespace CryptoExchange.Net.Authentication
/// <param name="auth">If the requests should be authenticated</param>
/// <param name="arraySerialization">Array serialization type</param>
/// <param name="parameterPosition">The position where the providedParameters should go</param>
/// <param name="requestBodyFormat">The formatting of the request body</param>
/// <param name="uriParameters">Parameters that need to be in the Uri of the request. Should include the provided parameters if they should go in the uri</param>
/// <param name="bodyParameters">Parameters that need to be in the body of the request. Should include the provided parameters if they should go in the body</param>
/// <param name="headers">The headers that should be send with the request</param>
@@ -60,7 +58,6 @@ namespace CryptoExchange.Net.Authentication
bool auth,
ArrayParametersSerialization arraySerialization,
HttpMethodParameterPosition parameterPosition,
RequestBodyFormat requestBodyFormat,
out SortedDictionary<string, object> uriParameters,
out SortedDictionary<string, object> bodyParameters,
out Dictionary<string, string> headers
@@ -248,7 +245,7 @@ namespace CryptoExchange.Net.Authentication
}
/// <summary>
/// HMACSHA256 sign the data and return the hash
/// HMACSHA512 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
@@ -270,7 +267,7 @@ namespace CryptoExchange.Net.Authentication
}
/// <summary>
/// HMACSHA384 sign the data and return the hash
/// HMACSHA512 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
+230 -1
View File
@@ -10,8 +10,10 @@ using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net.Clients
namespace CryptoExchange.Net
{
/// <summary>
/// Base API for all API clients
@@ -33,6 +35,37 @@ namespace CryptoExchange.Net.Clients
/// </summary>
public AuthenticationProvider? AuthenticationProvider { get; private set; }
/// <summary>
/// Where to put the parameters for requests with different Http methods
/// </summary>
public Dictionary<HttpMethod, HttpMethodParameterPosition> ParameterPositions { get; set; } = new Dictionary<HttpMethod, HttpMethodParameterPosition>
{
{ HttpMethod.Get, HttpMethodParameterPosition.InUri },
{ HttpMethod.Post, HttpMethodParameterPosition.InBody },
{ HttpMethod.Delete, HttpMethodParameterPosition.InBody },
{ HttpMethod.Put, HttpMethodParameterPosition.InBody }
};
/// <summary>
/// Request body content type
/// </summary>
public RequestBodyFormat requestBodyFormat = RequestBodyFormat.Json;
/// <summary>
/// Whether or not we need to manually parse an error instead of relying on the http status code
/// </summary>
public bool manualParseError = false;
/// <summary>
/// How to serialize array parameters when making requests
/// </summary>
public ArrayParametersSerialization arraySerialization = ArrayParametersSerialization.Array;
/// <summary>
/// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody)
/// </summary>
public string requestBodyEmptyContent = "{}";
/// <summary>
/// The environment this client communicates to
/// </summary>
@@ -43,6 +76,11 @@ namespace CryptoExchange.Net.Clients
/// </summary>
public bool OutputOriginalData { get; }
/// <summary>
/// The default serializer
/// </summary>
protected virtual JsonSerializer DefaultSerializer { get; set; } = JsonSerializer.Create(SerializerOptions.Default);
/// <summary>
/// Api options
/// </summary>
@@ -95,6 +133,197 @@ namespace CryptoExchange.Net.Clients
}
}
/// <summary>
/// Tries to parse the json data and return a JToken, validating the input not being empty and being valid json
/// </summary>
/// <param name="data">The data to parse</param>
/// <returns></returns>
protected CallResult<JToken> ValidateJson(string data)
{
if (string.IsNullOrEmpty(data))
{
var info = "Empty data object received";
_logger.Log(LogLevel.Error, info);
return new CallResult<JToken>(new DeserializeError(info, data));
}
try
{
return new CallResult<JToken>(JToken.Parse(data));
}
catch (JsonReaderException jre)
{
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
return new CallResult<JToken>(new DeserializeError(info, data));
}
catch (JsonSerializationException jse)
{
var info = $"Deserialize JsonSerializationException: {jse.Message}";
return new CallResult<JToken>(new DeserializeError(info, data));
}
catch (Exception ex)
{
var exceptionInfo = ex.ToLogString();
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
return new CallResult<JToken>(new DeserializeError(info, data));
}
}
/// <summary>
/// Deserialize a string into an object
/// </summary>
/// <typeparam name="T">The type to deserialize into</typeparam>
/// <param name="data">The data to deserialize</param>
/// <param name="serializer">A specific serializer to use</param>
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
/// <returns></returns>
protected CallResult<T> Deserialize<T>(string data, JsonSerializer? serializer = null, int? requestId = null)
{
var tokenResult = ValidateJson(data);
if (!tokenResult)
{
_logger.Log(LogLevel.Error, tokenResult.Error!.Message);
return new CallResult<T>(tokenResult.Error);
}
return Deserialize<T>(tokenResult.Data, serializer, requestId);
}
/// <summary>
/// Deserialize a JToken into an object
/// </summary>
/// <typeparam name="T">The type to deserialize into</typeparam>
/// <param name="obj">The data to deserialize</param>
/// <param name="serializer">A specific serializer to use</param>
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
/// <returns></returns>
protected CallResult<T> Deserialize<T>(JToken obj, JsonSerializer? serializer = null, int? requestId = null)
{
serializer ??= DefaultSerializer;
try
{
return new CallResult<T>(obj.ToObject<T>(serializer)!);
}
catch (JsonReaderException jre)
{
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message} Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {obj}";
_logger.Log(LogLevel.Error, info);
return new CallResult<T>(new DeserializeError(info, obj));
}
catch (JsonSerializationException jse)
{
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message} data: {obj}";
_logger.Log(LogLevel.Error, info);
return new CallResult<T>(new DeserializeError(info, obj));
}
catch (Exception ex)
{
var exceptionInfo = ex.ToLogString();
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {obj}";
_logger.Log(LogLevel.Error, info);
return new CallResult<T>(new DeserializeError(info, obj));
}
}
/// <summary>
/// Deserialize a stream into an object
/// </summary>
/// <typeparam name="T">The type to deserialize into</typeparam>
/// <param name="stream">The stream to deserialize</param>
/// <param name="serializer">A specific serializer to use</param>
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
/// <param name="elapsedMilliseconds">Milliseconds response time for the request this stream is a response for</param>
/// <returns></returns>
protected async Task<CallResult<T>> DeserializeAsync<T>(Stream stream, JsonSerializer? serializer = null, int? requestId = null, long? elapsedMilliseconds = null)
{
serializer ??= DefaultSerializer;
string? data = null;
try
{
// Let the reader keep the stream open so we're able to seek if needed. The calling method will close the stream.
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
// If we have to output the original json data or output the data into the logging we'll have to read to full response
// in order to log/return the json data
if (OutputOriginalData == true)
{
data = await reader.ReadToEndAsync().ConfigureAwait(false);
_logger.Log(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] " : "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms: " + data);
var result = Deserialize<T>(data, serializer, requestId);
result.OriginalData = data;
return result;
}
// If we don't have to keep track of the original json data we can use the JsonTextReader to deserialize the stream directly
// into the desired object, which has increased performance over first reading the string value into memory and deserializing from that
using var jsonReader = new JsonTextReader(reader);
_logger.Log(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] " : "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms");
return new CallResult<T>(serializer.Deserialize<T>(jsonReader)!);
}
catch (JsonReaderException jre)
{
if (data == null)
{
if (stream.CanSeek)
{
// If we can seek the stream rewind it so we can retrieve the original data that was sent
stream.Seek(0, SeekOrigin.Begin);
data = await ReadStreamAsync(stream).ConfigureAwait(false);
}
else
{
data = "[Data only available in Trace LogLevel]";
}
}
_logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}");
return new CallResult<T>(new DeserializeError($"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}", data));
}
catch (JsonSerializationException jse)
{
if (data == null)
{
if (stream.CanSeek)
{
stream.Seek(0, SeekOrigin.Begin);
data = await ReadStreamAsync(stream).ConfigureAwait(false);
}
else
{
data = "[Data only available in Trace LogLevel]";
}
}
_logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}");
return new CallResult<T>(new DeserializeError($"Deserialize JsonSerializationException: {jse.Message}", data));
}
catch (Exception ex)
{
if (data == null)
{
if (stream.CanSeek)
{
stream.Seek(0, SeekOrigin.Begin);
data = await ReadStreamAsync(stream).ConfigureAwait(false);
}
else
{
data = "[Data only available in Trace LogLevel]";
}
}
var exceptionInfo = ex.ToLogString();
_logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}");
return new CallResult<T>(new DeserializeError($"Deserialize Unknown Exception: {exceptionInfo}", data));
}
}
private static async Task<string> ReadStreamAsync(Stream stream)
{
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
return await reader.ReadToEndAsync().ConfigureAwait(false);
}
/// <summary>
/// Dispose
/// </summary>
+9 -9
View File
@@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.Clients
namespace CryptoExchange.Net
{
/// <summary>
/// The base for all clients, websocket client and rest client
@@ -15,7 +15,7 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// The name of the API the client is for
/// </summary>
public string Exchange { get; }
internal string Name { get; }
/// <summary>
/// Api clients in this client
@@ -26,7 +26,7 @@ namespace CryptoExchange.Net.Clients
/// The log object
/// </summary>
protected internal ILogger _logger;
/// <summary>
/// Provided client options
/// </summary>
@@ -36,14 +36,14 @@ namespace CryptoExchange.Net.Clients
/// ctor
/// </summary>
/// <param name="logger">Logger</param>
/// <param name="exchange">The name of the exchange this client is for</param>
/// <param name="name">The name of the API this client is for</param>
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
protected BaseClient(ILoggerFactory? logger, string exchange)
protected BaseClient(ILoggerFactory? logger, string name)
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
{
_logger = logger?.CreateLogger(exchange) ?? NullLoggerFactory.Instance.CreateLogger(exchange);
_logger = logger?.CreateLogger(name) ?? NullLoggerFactory.Instance.CreateLogger(name);
Exchange = exchange;
Name = name;
}
/// <summary>
@@ -57,7 +57,7 @@ namespace CryptoExchange.Net.Clients
throw new ArgumentNullException(nameof(options));
ClientOptions = options;
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {Exchange}.Net: v{GetType().Assembly.GetName().Version}");
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {Name}.Net: v{GetType().Assembly.GetName().Version}");
}
/// <summary>
@@ -74,7 +74,7 @@ namespace CryptoExchange.Net.Clients
/// Register an API client
/// </summary>
/// <param name="apiClient">The client</param>
protected T AddApiClient<T>(T apiClient) where T : BaseApiClient
protected T AddApiClient<T>(T apiClient) where T: BaseApiClient
{
if (ClientOptions == null)
throw new InvalidOperationException("Client should have called Initialize before adding API clients");
+1 -1
View File
@@ -2,7 +2,7 @@ using System.Linq;
using CryptoExchange.Net.Interfaces;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Clients
namespace CryptoExchange.Net
{
/// <summary>
/// Base rest client
+12 -13
View File
@@ -4,24 +4,23 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Clients
namespace CryptoExchange.Net
{
/// <summary>
/// Base for socket client implementations
/// </summary>
public abstract class BaseSocketClient : BaseClient, ISocketClient
public abstract class BaseSocketClient: BaseClient, ISocketClient
{
#region fields
/// <summary>
/// If client is disposing
/// </summary>
protected bool _disposing;
/// <inheritdoc />
public int CurrentConnections => ApiClients.OfType<SocketApiClient>().Sum(c => c.CurrentConnections);
/// <inheritdoc />
@@ -34,8 +33,8 @@ namespace CryptoExchange.Net.Clients
/// ctor
/// </summary>
/// <param name="logger">Logger</param>
/// <param name="exchange">The name of the exchange this client is for</param>
protected BaseSocketClient(ILoggerFactory? logger, string exchange) : base(logger, exchange)
/// <param name="name">The name of the API this client is for</param>
protected BaseSocketClient(ILoggerFactory? logger, string name) : base(logger, name)
{
}
@@ -46,11 +45,11 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns>
public virtual async Task UnsubscribeAsync(int subscriptionId)
{
foreach (var socket in ApiClients.OfType<SocketApiClient>())
foreach(var socket in ApiClients.OfType<SocketApiClient>())
{
var result = await socket.UnsubscribeAsync(subscriptionId).ConfigureAwait(false);
if (result)
break;
break;
}
}
@@ -64,7 +63,7 @@ namespace CryptoExchange.Net.Clients
if (subscription == null)
throw new ArgumentNullException(nameof(subscription));
_logger.UnsubscribingSubscription(subscription.SocketId, subscription.Id);
_logger.Log(LogLevel.Information, $"Socket {subscription.SocketId} Unsubscribing subscription " + subscription.Id);
await subscription.CloseAsync().ConfigureAwait(false);
}
@@ -74,10 +73,10 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns>
public virtual async Task UnsubscribeAllAsync()
{
var tasks = new List<Task>();
var tasks = new List<Task>();
foreach (var client in ApiClients.OfType<SocketApiClient>())
tasks.Add(client.UnsubscribeAllAsync());
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
}
@@ -87,7 +86,7 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns>
public virtual async Task ReconnectAsync()
{
_logger.ReconnectingAllConnections(CurrentConnections);
_logger.Log(LogLevel.Information, $"Reconnecting all {CurrentConnections} connections");
var tasks = new List<Task>();
foreach (var client in ApiClients.OfType<SocketApiClient>())
{
@@ -42,6 +42,6 @@ namespace CryptoExchange.Net.Clients
/// </summary>
/// <param name="exchangeName"></param>
/// <returns></returns>
public ISpotClient? SpotClient(string exchangeName) => _serviceProvider?.GetServices<ISpotClient>()?.SingleOrDefault(s => s.ExchangeName.Equals(exchangeName, StringComparison.InvariantCultureIgnoreCase));
public ISpotClient? SpotClient(string exchangeName) => _serviceProvider.GetServices<ISpotClient>()?.SingleOrDefault(s => s.ExchangeName.Equals(exchangeName, StringComparison.InvariantCultureIgnoreCase));
}
}
+126 -121
View File
@@ -8,15 +8,15 @@ using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Converters.JsonNet;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Requests;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net.Clients
namespace CryptoExchange.Net
{
/// <summary>
/// Base rest API client for interacting with a REST API
@@ -35,21 +35,6 @@ namespace CryptoExchange.Net.Clients
/// <inheritdoc />
public int TotalRequestsMade { get; set; }
/// <summary>
/// Request body content type
/// </summary>
protected RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json;
/// <summary>
/// How to serialize array parameters when making requests
/// </summary>
protected ArrayParametersSerialization ArraySerialization = ArrayParametersSerialization.Array;
/// <summary>
/// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody)
/// </summary>
protected string RequestBodyEmptyContent = "{}";
/// <summary>
/// Request headers to be sent with each request
/// </summary>
@@ -60,24 +45,12 @@ namespace CryptoExchange.Net.Clients
/// </summary>
internal IEnumerable<IRateLimiter> RateLimiters { get; }
/// <summary>
/// Where to put the parameters for requests with different Http methods
/// </summary>
public Dictionary<HttpMethod, HttpMethodParameterPosition> ParameterPositions { get; set; } = new Dictionary<HttpMethod, HttpMethodParameterPosition>
{
{ HttpMethod.Get, HttpMethodParameterPosition.InUri },
{ HttpMethod.Post, HttpMethodParameterPosition.InBody },
{ HttpMethod.Delete, HttpMethodParameterPosition.InBody },
{ HttpMethod.Put, HttpMethodParameterPosition.InBody }
};
/// <inheritdoc />
public new RestExchangeOptions ClientOptions => (RestExchangeOptions)base.ClientOptions;
/// <inheritdoc />
public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions;
/// <summary>
/// ctor
/// </summary>
@@ -86,9 +59,9 @@ namespace CryptoExchange.Net.Clients
/// <param name="baseAddress">Base address for this API client</param>
/// <param name="options">The base client options</param>
/// <param name="apiOptions">The Api client options</param>
public RestApiClient(ILogger logger, HttpClient? httpClient, string baseAddress, RestExchangeOptions options, RestApiOptions apiOptions)
: base(logger,
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
public RestApiClient(ILogger logger, HttpClient? httpClient, string baseAddress, RestExchangeOptions options, RestApiOptions apiOptions)
: base(logger,
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
apiOptions.ApiCredentials ?? options.ApiCredentials,
baseAddress,
options,
@@ -102,18 +75,6 @@ namespace CryptoExchange.Net.Clients
RequestFactory.Configure(options.Proxy, options.RequestTimeout, httpClient);
}
/// <summary>
/// Create a message accessor instance
/// </summary>
/// <returns></returns>
protected virtual IStreamMessageAccessor CreateAccessor() => new JsonNetStreamMessageAccessor();
/// <summary>
/// Create a serializer instance
/// </summary>
/// <returns></returns>
protected virtual IMessageSerializer CreateSerializer() => new JsonNetMessageSerializer();
/// <summary>
/// Execute a request to the uri and returns if it was successful
/// </summary>
@@ -126,6 +87,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
/// <param name="requestWeight">Credits used for the request</param>
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
/// <param name="additionalHeaders">Additional headers to send with the request</param>
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
/// <returns></returns>
@@ -140,6 +102,7 @@ namespace CryptoExchange.Net.Clients
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null,
int requestWeight = 1,
JsonSerializer? deserializer = null,
Dictionary<string, string>? additionalHeaders = null,
bool ignoreRatelimit = false)
{
@@ -147,15 +110,15 @@ namespace CryptoExchange.Net.Clients
while (true)
{
currentTry++;
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
if (!request)
return new WebCallResult(request.Error!);
var result = await GetResponseAsync<object>(request.Data, cancellationToken).ConfigureAwait(false);
var result = await GetResponseAsync<object>(request.Data, deserializer, cancellationToken, true).ConfigureAwait(false);
if (!result)
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString());
_logger.Log(LogLevel.Warning, $"[Req {result.RequestId}] Error received in {result.ResponseTime!.Value.TotalMilliseconds}ms: {result.Error}");
else
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]");
_logger.Log(LogLevel.Debug, $"[Req {result.RequestId}] Response received in {result.ResponseTime!.Value.TotalMilliseconds}ms{(OutputOriginalData ? (": " + result.OriginalData) : "")}");
if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false))
continue;
@@ -177,6 +140,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
/// <param name="requestWeight">Credits used for the request</param>
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
/// <param name="additionalHeaders">Additional headers to send with the request</param>
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
/// <returns></returns>
@@ -191,6 +155,7 @@ namespace CryptoExchange.Net.Clients
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null,
int requestWeight = 1,
JsonSerializer? deserializer = null,
Dictionary<string, string>? additionalHeaders = null,
bool ignoreRatelimit = false
) where T : class
@@ -199,15 +164,15 @@ namespace CryptoExchange.Net.Clients
while (true)
{
currentTry++;
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
if (!request)
return new WebCallResult<T>(request.Error!);
var result = await GetResponseAsync<T>(request.Data, cancellationToken).ConfigureAwait(false);
var result = await GetResponseAsync<T>(request.Data, deserializer, cancellationToken, false).ConfigureAwait(false);
if (!result)
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString());
_logger.Log(LogLevel.Warning, $"[Req {result.RequestId}] Error received in {result.ResponseTime!.Value.TotalMilliseconds}ms: {result.Error}");
else
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]");
_logger.Log(LogLevel.Debug, $"[Req {result.RequestId}] Response received in {result.ResponseTime!.Value.TotalMilliseconds}ms{(OutputOriginalData ? (": " + result.OriginalData) : "")}");
if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false))
continue;
@@ -228,6 +193,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
/// <param name="requestWeight">Credits used for the request</param>
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
/// <param name="additionalHeaders">Additional headers to send with the request</param>
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
/// <returns></returns>
@@ -241,6 +207,7 @@ namespace CryptoExchange.Net.Clients
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null,
int requestWeight = 1,
JsonSerializer? deserializer = null,
Dictionary<string, string>? additionalHeaders = null,
bool ignoreRatelimit = false)
{
@@ -257,7 +224,7 @@ namespace CryptoExchange.Net.Clients
var syncTimeResult = await syncTask.ConfigureAwait(false);
if (!syncTimeResult)
{
_logger.RestApiFailedToSyncTime(requestId, syncTimeResult.Error!.ToString());
_logger.Log(LogLevel.Debug, $"[Req {requestId}] Failed to sync time, aborting request: " + syncTimeResult.Error);
return syncTimeResult.As<IRequest>(default);
}
}
@@ -275,13 +242,13 @@ namespace CryptoExchange.Net.Clients
if (signed && AuthenticationProvider == null)
{
_logger.RestApiNoApiCredentials(requestId, uri.AbsolutePath);
_logger.Log(LogLevel.Warning, $"[Req {requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
return new CallResult<IRequest>(new NoApiCredentialsError());
}
_logger.RestApiCreatingRequest(requestId, uri);
_logger.Log(LogLevel.Information, $"[Req {requestId}] Creating request for " + uri);
var paramsPosition = parameterPosition ?? ParameterPositions[method];
var request = ConstructRequest(uri, method, parameters?.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value), signed, paramsPosition, arraySerialization ?? ArraySerialization, requestBodyFormat ?? RequestBodyFormat, requestId, additionalHeaders);
var request = ConstructRequest(uri, method, parameters?.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value), signed, paramsPosition, arraySerialization ?? this.arraySerialization, requestBodyFormat ?? this.requestBodyFormat, requestId, additionalHeaders);
string? paramString = "";
if (paramsPosition == HttpMethodParameterPosition.InBody)
@@ -292,7 +259,7 @@ namespace CryptoExchange.Net.Clients
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
TotalRequestsMade++;
_logger.RestApiSendingRequest(requestId, method, signed ? "signed": "", request.Uri, paramString);
_logger.Log(LogLevel.Trace, $"[Req {requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}");
return new CallResult<IRequest>(request);
}
@@ -300,64 +267,109 @@ namespace CryptoExchange.Net.Clients
/// Executes the request and returns the result deserialized into the type parameter class
/// </summary>
/// <param name="request">The request object to execute</param>
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="expectedEmptyResponse">If an empty response is expected</param>
/// <returns></returns>
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(
IRequest request,
CancellationToken cancellationToken)
JsonSerializer? deserializer,
CancellationToken cancellationToken,
bool expectedEmptyResponse)
{
var sw = Stopwatch.StartNew();
Stream? responseStream = null;
IResponse? response = null;
IStreamMessageAccessor? accessor = null;
try
{
response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
var response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
sw.Stop();
var statusCode = response.StatusCode;
var headers = response.ResponseHeaders;
var responseLength = response.ContentLength;
responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData;
accessor = CreateAccessor();
if (!response.IsSuccessStatusCode)
var responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
// Error response
await accessor.Read(responseStream, true).ConfigureAwait(false);
// If we have to manually parse error responses (can't rely on HttpStatusCode) we'll need to read the full
// response before being able to deserialize it into the resulting type since we don't know if its an error response or data
if (manualParseError)
{
using var reader = new StreamReader(responseStream);
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
responseLength ??= data.Length;
responseStream.Close();
response.Close();
if (!expectedEmptyResponse)
{
// Validate if it is valid json. Sometimes other data will be returned, 502 error html pages for example
var parseResult = ValidateJson(data);
if (!parseResult.Success)
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
// Let the library implementation see if it is an error response, and if so parse the error
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
if (error != null)
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
// Not an error, so continue deserializing
var deserializeResult = Deserialize<T>(parseResult.Data, deserializer, request.RequestId);
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error);
}
else
{
if (!string.IsNullOrEmpty(data))
{
var parseResult = ValidateJson(data);
if (!parseResult.Success)
// Not empty, and not json
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
if (error != null)
// Error response
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
}
// Empty success response; okay
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, default);
}
}
else
{
if (expectedEmptyResponse)
{
// We expected an empty response and the request is successful and don't manually parse errors, so assume it's correct
responseStream.Close();
response.Close();
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, null);
}
// Success status code, and we don't have to check for errors. Continue deserializing directly from the stream
var desResult = await DeserializeAsync<T>(responseStream, deserializer, request.RequestId, sw.ElapsedMilliseconds).ConfigureAwait(false);
responseStream.Close();
response.Close();
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, responseLength, OutputOriginalData ? desResult.OriginalData : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), desResult.Data, desResult.Error);
}
}
else
{
// Http status code indicates error
using var reader = new StreamReader(responseStream);
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
responseStream.Close();
response.Close();
Error error;
if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429)
error = ParseRateLimitResponse((int)response.StatusCode, response.ResponseHeaders, accessor);
error = ParseRateLimitResponse((int)response.StatusCode, response.ResponseHeaders, data);
else
error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor);
error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, data);
if (error.Code == null || error.Code == 0)
error.Code = (int)response.StatusCode;
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data.Length, data, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
}
if (typeof(T) == typeof(object))
// Success status code and expected empty response, assume it's correct
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, null);
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
if (!valid)
{
// Invalid json
var error = new ServerError(accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]");
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
}
// Json response received
var parsedError = TryParseError(accessor);
if (parsedError != null)
// Success status code, but TryParseError determined it was an error response
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parsedError);
var deserializeResult = accessor.Deserialize<T>();
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error);
}
catch (HttpRequestException requestException)
{
@@ -378,12 +390,6 @@ namespace CryptoExchange.Net.Clients
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError($"Request timed out"));
}
}
finally
{
accessor?.Clear();
responseStream?.Close();
response?.Close();
}
}
/// <summary>
@@ -391,9 +397,12 @@ namespace CryptoExchange.Net.Clients
/// When setting manualParseError to true 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="accessor">Data accessor</param>
/// <param name="data">Received data</param>
/// <returns>Null if not an error, Error otherwise</returns>
protected virtual ServerError? TryParseError(IMessageAccessor accessor) => null;
protected virtual Task<ServerError?> TryParseErrorAsync(JToken data)
{
return Task.FromResult<ServerError?>(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.
@@ -459,7 +468,6 @@ namespace CryptoExchange.Net.Clients
signed,
arraySerialization,
parameterPosition,
bodyFormat,
out uriParameters,
out bodyParameters,
out headers);
@@ -511,7 +519,7 @@ namespace CryptoExchange.Net.Clients
if (bodyParameters.Any())
WriteParamBody(request, bodyParameters, contentType);
else
request.SetContent(RequestBodyEmptyContent, contentType);
request.SetContent(requestBodyEmptyContent, contentType);
}
return request;
@@ -528,7 +536,7 @@ namespace CryptoExchange.Net.Clients
if (contentType == Constants.JsonContentHeader)
{
// Write the parameters as json in the body
var stringData = CreateSerializer().Serialize(parameters);
var stringData = JsonConvert.SerializeObject(parameters);
request.SetContent(stringData, contentType);
}
else if (contentType == Constants.FormContentHeader)
@@ -544,38 +552,35 @@ namespace CryptoExchange.Net.Clients
/// </summary>
/// <param name="httpStatusCode">The response status code</param>
/// <param name="responseHeaders">The response headers</param>
/// <param name="accessor">Data accessor</param>
/// <param name="data">The response data</param>
/// <returns></returns>
protected virtual Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, IMessageAccessor accessor)
protected virtual Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, string data)
{
var message = accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Error response content only available when OutputOriginal = true in client options]";
return new ServerError(message);
return new ServerError(data);
}
/// <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>
/// <param name="data">The response data</param>
/// <returns></returns>
protected virtual Error ParseRateLimitResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, IMessageAccessor accessor)
protected virtual Error ParseRateLimitResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, string data)
{
var message = accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Error response content only available when OutputOriginal = true in client options]";
// Handle retry after header
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
if (retryAfterHeader.Value?.Any() != true)
return new ServerRateLimitError(message);
return new ServerRateLimitError(data);
var value = retryAfterHeader.Value.First();
if (int.TryParse(value, out var seconds))
return new ServerRateLimitError(message) { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) };
return new ServerRateLimitError(data) { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) };
if (DateTime.TryParse(value, out var datetime))
return new ServerRateLimitError(message) { RetryAfter = datetime };
return new ServerRateLimitError(data) { RetryAfter = datetime };
return new ServerRateLimitError(message);
return new ServerRateLimitError(data);
}
/// <summary>
@@ -592,7 +597,7 @@ namespace CryptoExchange.Net.Clients
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
{
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
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, true, null);
@@ -619,7 +624,7 @@ namespace CryptoExchange.Net.Clients
}
// Calculate time offset between local and server
var offset = result.Data - localTime.AddMilliseconds(result.ResponseTime!.Value.TotalMilliseconds / 2);
var offset = result.Data - (localTime.AddMilliseconds(result.ResponseTime!.Value.TotalMilliseconds / 2));
timeSyncParams.UpdateTimeOffset(offset);
timeSyncParams.TimeSyncState.Semaphore.Release();
}
+50 -60
View File
@@ -1,21 +1,23 @@
using CryptoExchange.Net.Converters.JsonNet;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Clients
namespace CryptoExchange.Net
{
/// <summary>
/// Base socket API client for interaction with a websocket API
@@ -61,11 +63,6 @@ namespace CryptoExchange.Net.Clients
/// </summary>
protected internal IEnumerable<IRateLimiter>? RateLimiters { get; set; }
/// <summary>
/// The max size a websocket message size can be
/// </summary>
protected internal int? MessageSendSizeLimit { get; set; }
/// <summary>
/// Periodic task regisrations
/// </summary>
@@ -113,8 +110,8 @@ namespace CryptoExchange.Net.Clients
/// <param name="options">Client options</param>
/// <param name="baseAddress">Base address for this API client</param>
/// <param name="apiOptions">The Api client options</param>
public SocketApiClient(ILogger logger, string baseAddress, SocketExchangeOptions options, SocketApiOptions apiOptions)
: base(logger,
public SocketApiClient(ILogger logger, string baseAddress, SocketExchangeOptions options, SocketApiOptions apiOptions)
: base(logger,
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
apiOptions.ApiCredentials ?? options.ApiCredentials,
baseAddress,
@@ -127,18 +124,6 @@ namespace CryptoExchange.Net.Clients
RateLimiters = rateLimiters;
}
/// <summary>
/// Create a message accessor instance
/// </summary>
/// <returns></returns>
protected internal virtual IByteMessageAccessor CreateAccessor() => new JsonNetByteMessageAccessor();
/// <summary>
/// Create a serializer instance
/// </summary>
/// <returns></returns>
protected internal virtual IMessageSerializer CreateSerializer() => new JsonNetMessageSerializer();
/// <summary>
/// Add a query to periodically send on each connection
/// </summary>
@@ -206,13 +191,12 @@ namespace CryptoExchange.Net.Clients
return socketResult.As<UpdateSubscription>(null);
socketConnection = socketResult.Data;
subscription.HandleUpdatesBeforeConfirmation = subscription.HandleUpdatesBeforeConfirmation || HandleMessageBeforeConfirmation;
// Add a subscription on the socket connection
var success = socketConnection.AddSubscription(subscription);
var success = socketConnection.CanAddSubscription();
if (!success)
{
_logger.FailedToAddSubscriptionRetryOnDifferentConnection(socketConnection.SocketId);
_logger.Log(LogLevel.Trace, $"[Sckt {socketConnection.SocketId}] failed to add subscription, retrying on different connection");
continue;
}
@@ -240,23 +224,27 @@ namespace CryptoExchange.Net.Clients
if (socketConnection.PausedActivity)
{
_logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId);
_logger.Log(LogLevel.Warning, $"[Sckt {socketConnection.SocketId}] has been paused, can't subscribe at this moment");
return new CallResult<UpdateSubscription>(new ServerError("Socket is paused"));
}
var waitEvent = new ManualResetEvent(false);
var waitEvent = new AsyncResetEvent(false);
var subQuery = subscription.GetSubQuery(socketConnection);
if (subQuery != null)
{
if (HandleMessageBeforeConfirmation)
socketConnection.AddSubscription(subscription);
// Send the request and wait for answer
var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, waitEvent).ConfigureAwait(false);
if (!subResult)
{
waitEvent?.Set();
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
_logger.Log(LogLevel.Warning, $"[Sckt {socketConnection.SocketId}] failed to subscribe: {subResult.Error}");
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
var unsubscribe = subResult.Error is CancellationRequestedError;
await socketConnection.CloseAsync(subscription, unsubscribe).ConfigureAwait(false);
return new CallResult<UpdateSubscription>(subResult.Error!);
}
@@ -268,13 +256,16 @@ namespace CryptoExchange.Net.Clients
{
subscription.CancellationTokenRegistration = ct.Register(async () =>
{
_logger.CancellationTokenSetClosingSubscription(socketConnection.SocketId, subscription.Id);
_logger.Log(LogLevel.Information, $"[Sckt {socketConnection.SocketId}] Cancellation token set, closing subscription {subscription.Id}");
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
}, false);
}
if (!HandleMessageBeforeConfirmation)
socketConnection.AddSubscription(subscription);
waitEvent?.Set();
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
_logger.Log(LogLevel.Information, $"[Sckt {socketConnection.SocketId}] subscription {subscription.Id} completed successfully");
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
}
@@ -331,7 +322,7 @@ namespace CryptoExchange.Net.Clients
if (socketConnection.PausedActivity)
{
_logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId);
_logger.Log(LogLevel.Warning, $"[Sckt {socketConnection.SocketId}] has been paused, can't send query at this moment");
return new CallResult<T>(new ServerError("Socket is paused"));
}
@@ -372,7 +363,7 @@ namespace CryptoExchange.Net.Clients
if (AuthenticationProvider == null)
return new CallResult<bool>(new NoApiCredentialsError());
_logger.AttemptingToAuthenticate(socket.SocketId);
_logger.Log(LogLevel.Debug, $"[Sckt {socket.SocketId}] Attempting to authenticate");
var authRequest = GetAuthenticationRequest();
if (authRequest != null)
{
@@ -380,7 +371,7 @@ namespace CryptoExchange.Net.Clients
if (!result)
{
_logger.AuthenticationFailed(socket.SocketId);
_logger.Log(LogLevel.Warning, $"[Sckt {socket.SocketId}] authentication failed");
if (socket.Connected)
await socket.CloseAsync().ConfigureAwait(false);
@@ -389,7 +380,7 @@ namespace CryptoExchange.Net.Clients
}
}
_logger.Authenticated(socket.SocketId);
_logger.Log(LogLevel.Debug, $"[Sckt {socket.SocketId}] authenticated");
socket.Authenticated = true;
return new CallResult<bool>(true);
}
@@ -433,13 +424,13 @@ namespace CryptoExchange.Net.Clients
}
/// <summary>
/// Update the subscription when the connection is restored after disconnecting. Can be used to update an authentication token for example.
/// Update the original request to send when the connection is restored after disconnecting. Can be used to update an authentication token for example.
/// </summary>
/// <param name="subscription">The subscription</param>
/// <param name="request">The original request</param>
/// <returns></returns>
protected internal virtual Task<CallResult> RevitalizeRequestAsync(Subscription subscription)
protected internal virtual Task<CallResult<object>> RevitalizeRequestAsync(object request)
{
return Task.FromResult(new CallResult(null));
return Task.FromResult(new CallResult<object>(request));
}
/// <summary>
@@ -452,25 +443,27 @@ namespace CryptoExchange.Net.Clients
{
var socketResult = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
&& s.Value.ApiClient.GetType() == GetType()
&& (s.Value.ApiClient.GetType() == GetType())
&& (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault();
var result = socketResult.Equals(default(KeyValuePair<int, SocketConnection>)) ? null : socketResult.Value;
if (result != null)
{
if (result.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget))
if (result.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
{
// 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>(result);
}
}
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
if (!connectionAddress)
{
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error?.ToString());
_logger.Log(LogLevel.Warning, $"Failed to determine connection url: " + connectionAddress.Error);
return connectionAddress.As<SocketConnection>(null);
}
if (connectionAddress.Data != address)
_logger.ConnectionAddressSetTo(connectionAddress.Data!);
_logger.Log(LogLevel.Debug, $"Connection address set to " + connectionAddress.Data);
// Create new socket
var socket = CreateSocket(connectionAddress.Data!);
@@ -534,7 +527,7 @@ namespace CryptoExchange.Net.Clients
protected virtual IWebsocket CreateSocket(string address)
{
var socket = SocketFactory.CreateWebsocket(_logger, GetWebSocketParameters(address));
_logger.SocketCreatedForAddress(socket.Id, address);
_logger.Log(LogLevel.Debug, $"[Sckt {socket.Id}] created for " + address);
return socket;
}
@@ -560,7 +553,7 @@ namespace CryptoExchange.Net.Clients
if (subscription == null || connection == null)
return false;
_logger.UnsubscribingSubscription(connection.SocketId, subscriptionId);
_logger.Log(LogLevel.Information, $"[Sckt {connection.SocketId}] unsubscribing subscription " + subscriptionId);
await connection.CloseAsync(subscription).ConfigureAwait(false);
return true;
}
@@ -575,7 +568,7 @@ namespace CryptoExchange.Net.Clients
if (subscription == null)
throw new ArgumentNullException(nameof(subscription));
_logger.UnsubscribingSubscription(subscription.SocketId, subscription.Id);
_logger.Log(LogLevel.Information, $"[Sckt {subscription.SocketId}] Unsubscribing subscription " + subscription.Id);
await subscription.CloseAsync().ConfigureAwait(false);
}
@@ -589,12 +582,12 @@ namespace CryptoExchange.Net.Clients
if (sum == 0)
return;
_logger.UnsubscribingAll(socketConnections.Sum(s => s.Value.UserSubscriptionCount));
_logger.Log(LogLevel.Information, $"Unsubscribing all {socketConnections.Sum(s => s.Value.UserSubscriptionCount)} subscriptions");
var tasks = new List<Task>();
{
var socketList = socketConnections.Values;
foreach (var sub in socketList)
tasks.Add(sub.CloseAsync());
tasks.Add(sub.CloseAsync());
}
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
@@ -606,7 +599,7 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns>
public virtual async Task ReconnectAsync()
{
_logger.ReconnectingAllConnections(socketConnections.Count);
_logger.Log(LogLevel.Information, $"Reconnecting all {socketConnections.Count} connections");
var tasks = new List<Task>();
{
var socketList = socketConnections.Values;
@@ -620,7 +613,7 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// Log the current state of connections and subscriptions
/// </summary>
public string GetSubscriptionsState(bool includeSubDetails = true)
public string GetSubscriptionsState()
{
var sb = new StringBuilder();
sb.AppendLine($"{GetType().Name}");
@@ -636,15 +629,12 @@ namespace CryptoExchange.Net.Clients
sb.AppendLine($" Authenticated: {connection.Value.Authenticated}");
sb.AppendLine($" Download speed: {connection.Value.IncomingKbps} kbps");
sb.AppendLine($" Subscriptions:");
if (includeSubDetails)
foreach (var subscription in connection.Value.Subscriptions)
{
foreach (var subscription in connection.Value.Subscriptions)
{
sb.AppendLine($" Id: {subscription.Id}");
sb.AppendLine($" Confirmed: {subscription.Confirmed}");
sb.AppendLine($" Invocations: {subscription.TotalInvocations}");
sb.AppendLine($" Identifiers: [{string.Join(", ", subscription.ListenerIdentifiers)}]");
}
sb.AppendLine($" Id: {subscription.Id}");
sb.AppendLine($" Confirmed: {subscription.Confirmed}");
sb.AppendLine($" Invocations: {subscription.TotalInvocations}");
sb.AppendLine($" Identifiers: [{string.Join(", ", subscription.ListenerIdentifiers)}]");
}
}
return sb.ToString();
@@ -658,7 +648,7 @@ namespace CryptoExchange.Net.Clients
_disposing = true;
if (socketConnections.Sum(s => s.Value.UserSubscriptionCount) > 0)
{
_logger.DisposingSocketClient();
_logger.Log(LogLevel.Debug, "Disposing socket client, closing all subscriptions");
_ = UnsubscribeAllAsync();
}
semaphoreSlim?.Dispose();
@@ -676,8 +666,8 @@ namespace CryptoExchange.Net.Clients
/// Preprocess a stream message
/// </summary>
/// <param name="type"></param>
/// <param name="data"></param>
/// <param name="stream"></param>
/// <returns></returns>
public virtual ReadOnlyMemory<byte> PreprocessStreamMessage(WebSocketMessageType type, ReadOnlyMemory<byte> data) => data;
public virtual Stream PreprocessStreamMessage(WebSocketMessageType type, Stream stream) => stream;
}
}
@@ -8,7 +8,7 @@ using CryptoExchange.Net.Attributes;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net.Converters.JsonNet
namespace CryptoExchange.Net.Converters
{
/// <summary>
/// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties
@@ -192,4 +192,25 @@ namespace CryptoExchange.Net.Converters.JsonNet
private static T? GetCustomAttribute<T>(Type type) where T : Attribute =>
(T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T)));
}
/// <summary>
/// Mark property as an index in the array
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class ArrayPropertyAttribute: Attribute
{
/// <summary>
/// The index in the array
/// </summary>
public int Index { get; }
/// <summary>
/// ctor
/// </summary>
/// <param name="index"></param>
public ArrayPropertyAttribute(int index)
{
Index = index;
}
}
}
@@ -1,27 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Converters
{
/// <summary>
/// Mark property as an index in the array
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class ArrayPropertyAttribute : Attribute
{
/// <summary>
/// The index in the array
/// </summary>
public int Index { get; }
/// <summary>
/// ctor
/// </summary>
/// <param name="index"></param>
public ArrayPropertyAttribute(int index)
{
Index = index;
}
}
}
@@ -4,7 +4,7 @@ using System.Diagnostics;
using System.Linq;
using Newtonsoft.Json;
namespace CryptoExchange.Net.Converters.JsonNet
namespace CryptoExchange.Net.Converters
{
/// <summary>
/// Base class for enum converters
@@ -1,7 +1,7 @@
using System;
using Newtonsoft.Json;
namespace CryptoExchange.Net.Converters.JsonNet
namespace CryptoExchange.Net.Converters
{
/// <summary>
/// Boolean converter with support for "0"/"1" (strings)
@@ -4,7 +4,7 @@ using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
namespace CryptoExchange.Net.Converters.JsonNet
namespace CryptoExchange.Net.Converters
{
/// <summary>
/// Datetime converter. Supports converting from string/long/double to DateTime and back. Numbers are assumed to be the time since 1970-01-01.
@@ -26,12 +26,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (reader.Value == null)
{
if (objectType == typeof(DateTime))
return default(DateTime);
return null;
}
if(reader.TokenType is JsonToken.Integer)
{
@@ -61,6 +56,9 @@ namespace CryptoExchange.Net.Converters.JsonNet
else if(reader.TokenType is JsonToken.String)
{
var stringValue = (string)reader.Value;
if (string.IsNullOrWhiteSpace(stringValue))
return null;
if (string.IsNullOrWhiteSpace(stringValue)
|| stringValue == "-1"
|| (double.TryParse(stringValue, out var doubleVal) && doubleVal == 0))
@@ -2,7 +2,7 @@
using System;
using System.Globalization;
namespace CryptoExchange.Net.Converters.JsonNet
namespace Kraken.Net.Converters
{
/// <summary>
/// Converter for serializing decimal values as string
@@ -14,7 +14,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
/// <inheritdoc />
public override bool CanConvert(Type objectType) => objectType == typeof(decimal) || objectType == typeof(decimal?);
/// <inheritdoc />
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
@@ -7,7 +7,7 @@ using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace CryptoExchange.Net.Converters.JsonNet
namespace CryptoExchange.Net.Converters
{
/// <summary>
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
@@ -1,337 +0,0 @@
using CryptoExchange.Net.Converters.MessageParsing;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Converters.JsonNet
{
/// <summary>
/// Json.Net message accessor
/// </summary>
public abstract class JsonNetMessageAccessor : IMessageAccessor
{
/// <summary>
/// The json token loaded
/// </summary>
protected JToken? _token;
private static readonly JsonSerializer _serializer = JsonSerializer.Create(SerializerOptions.WithConverters);
/// <inheritdoc />
public bool IsJson { get; protected set; }
/// <inheritdoc />
public abstract bool OriginalDataAvailable { get; }
/// <inheritdoc />
public object? Underlying => _token;
/// <inheritdoc />
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
{
if (!IsJson)
return new CallResult<object>(GetOriginalString());
var source = _token;
if (path != null)
source = GetPathNode(path.Value);
try
{
var result = source!.ToObject(type, _serializer)!;
return new CallResult<object>(result);
}
catch (JsonReaderException jre)
{
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
}
catch (JsonSerializationException jse)
{
var info = $"Deserialize JsonSerializationException: {jse.Message}";
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
}
catch (Exception ex)
{
var exceptionInfo = ex.ToLogString();
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
}
}
/// <inheritdoc />
public CallResult<T> Deserialize<T>(MessagePath? path = null)
{
var source = _token;
if (path != null)
source = GetPathNode(path.Value);
try
{
var result = source!.ToObject<T>(_serializer)!;
return new CallResult<T>(result);
}
catch (JsonReaderException jre)
{
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
}
catch (JsonSerializationException jse)
{
var info = $"Deserialize JsonSerializationException: {jse.Message}";
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
}
catch (Exception ex)
{
var exceptionInfo = ex.ToLogString();
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
}
}
/// <inheritdoc />
public NodeType? GetNodeType()
{
if (!IsJson)
throw new InvalidOperationException("Can't access json data on non-json message");
if (_token == null)
return null;
if (_token.Type == JTokenType.Object)
return NodeType.Object;
if (_token.Type == JTokenType.Array)
return NodeType.Array;
return NodeType.Value;
}
/// <inheritdoc />
public NodeType? GetNodeType(MessagePath path)
{
if (!IsJson)
throw new InvalidOperationException("Can't access json data on non-json message");
var node = GetPathNode(path);
if (node == null)
return null;
if (node.Type == JTokenType.Object)
return NodeType.Object;
if (node.Type == JTokenType.Array)
return NodeType.Array;
return NodeType.Value;
}
/// <inheritdoc />
public T? GetValue<T>(MessagePath path)
{
if (!IsJson)
throw new InvalidOperationException("Can't access json data on non-json message");
var value = GetPathNode(path);
if (value == null)
return default;
if (value.Type == JTokenType.Object || value.Type == JTokenType.Array)
return default;
return value!.Value<T>();
}
/// <inheritdoc />
public List<T?>? GetValues<T>(MessagePath path)
{
if (!IsJson)
throw new InvalidOperationException("Can't access json data on non-json message");
var value = GetPathNode(path);
if (value == null)
return default;
if (value.Type == JTokenType.Object)
return default;
return value!.Values<T>().ToList();
}
private JToken? GetPathNode(MessagePath path)
{
if (!IsJson)
throw new InvalidOperationException("Can't access json data on non-json message");
var currentToken = _token;
foreach (var node in path)
{
if (node.Type == 0)
{
// Int value
var val = node.Index!.Value;
if (currentToken!.Type != JTokenType.Array || ((JArray)currentToken).Count <= val)
return null;
currentToken = currentToken[val];
}
else if (node.Type == 1)
{
// String value
if (currentToken!.Type != JTokenType.Object)
return null;
currentToken = currentToken[node.Property!];
}
else
{
// Property name
if (currentToken!.Type != JTokenType.Object)
return null;
currentToken = (currentToken.First as JProperty)?.Name;
}
if (currentToken == null)
return null;
}
return currentToken;
}
/// <inheritdoc />
public abstract string GetOriginalString();
/// <inheritdoc />
public abstract void Clear();
}
/// <summary>
/// Json.Net stream message accessor
/// </summary>
public class JsonNetStreamMessageAccessor : JsonNetMessageAccessor, IStreamMessageAccessor
{
private Stream? _stream;
/// <inheritdoc />
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
/// <inheritdoc />
public async Task<bool> Read(Stream stream, bool bufferStream)
{
if (bufferStream && stream is not MemoryStream)
{
// We need to be buffer the stream, and it's not currently a seekable stream, so copy it to a new memory stream
_stream = new MemoryStream();
stream.CopyTo(_stream);
_stream.Position = 0;
}
else if (bufferStream)
{
// We need to buffer the stream, and the current stream is seekable, store as is
_stream = stream;
}
else
{
// We don't need to buffer the stream, so don't bother keeping the reference
}
var readStream = _stream ?? stream;
var length = readStream.CanSeek ? readStream.Length : 4096;
using var reader = new StreamReader(readStream, Encoding.UTF8, false, (int)Math.Max(2, length), true);
using var jsonTextReader = new JsonTextReader(reader);
try
{
_token = await JToken.LoadAsync(jsonTextReader).ConfigureAwait(false);
IsJson = true;
}
catch (Exception)
{
// Not a json message
IsJson = false;
}
return IsJson;
}
/// <inheritdoc />
public override string GetOriginalString()
{
if (_stream is null)
throw new NullReferenceException("Stream not initialized");
_stream.Position = 0;
using var textReader = new StreamReader(_stream, Encoding.UTF8, false, 1024, true);
return textReader.ReadToEnd();
}
/// <inheritdoc />
public override void Clear()
{
_stream?.Dispose();
_stream = null;
_token = null;
}
}
/// <summary>
/// Json.Net byte message accessor
/// </summary>
public class JsonNetByteMessageAccessor : JsonNetMessageAccessor, IByteMessageAccessor
{
private ReadOnlyMemory<byte> _bytes;
/// <inheritdoc />
public bool Read(ReadOnlyMemory<byte> data)
{
_bytes = data;
// Try getting the underlying byte[] instead of the ToArray to prevent creating a copy
using var stream = MemoryMarshal.TryGetArray(data, out var arraySegment)
? new MemoryStream(arraySegment.Array, arraySegment.Offset, arraySegment.Count)
: new MemoryStream(data.ToArray());
using var reader = new StreamReader(stream, Encoding.UTF8, false, Math.Max(2, data.Length), true);
using var jsonTextReader = new JsonTextReader(reader);
try
{
_token = JToken.Load(jsonTextReader);
IsJson = true;
}
catch (Exception)
{
// Not a json message
IsJson = false;
}
return IsJson;
}
/// <inheritdoc />
public override string GetOriginalString() =>
// Netstandard 2.0 doesn't support GetString from a ReadonlySpan<byte>, so use ToArray there instead
#if NETSTANDARD2_0
Encoding.UTF8.GetString(_bytes.ToArray());
#else
Encoding.UTF8.GetString(_bytes.Span);
#endif
/// <inheritdoc />
public override bool OriginalDataAvailable => true;
/// <inheritdoc />
public override void Clear()
{
_bytes = null;
_token = null;
}
}
}
@@ -1,7 +1,7 @@
using Newtonsoft.Json;
using System.Globalization;
namespace CryptoExchange.Net.Converters.JsonNet
namespace CryptoExchange.Net.Converters
{
/// <summary>
/// Serializer options
@@ -1,133 +0,0 @@
using System;
using System.Collections.Concurrent;
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;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties
/// with [ArrayProperty(x)] where x is the index of the property in the array
/// </summary>
public class ArrayConverter : JsonConverterFactory
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert) => true;
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
Type converterType = typeof(ArrayConverterInner<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType);
}
private class ArrayPropertyInfo
{
public PropertyInfo PropertyInfo { get; set; } = null!;
public ArrayPropertyAttribute ArrayProperty { get; set; } = null!;
public Type? JsonConverterType { get; set; }
public bool DefaultDeserialization { get; set; }
}
private class ArrayConverterInner<T> : JsonConverter<T>
{
private static readonly ConcurrentDictionary<Type, List<ArrayPropertyInfo>> _typeAttributesCache = new ConcurrentDictionary<Type, List<ArrayPropertyInfo>>();
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
// TODO
throw new NotImplementedException();
}
/// <inheritdoc />
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return default;
var result = Activator.CreateInstance(typeToConvert);
return (T)ParseObject(ref reader, result, typeToConvert);
}
private static List<ArrayPropertyInfo> CacheTypeAttributes(Type type)
{
var attributes = new List<ArrayPropertyInfo>();
var properties = type.GetProperties();
foreach (var property in properties)
{
var att = property.GetCustomAttribute<ArrayPropertyAttribute>();
if (att == null)
continue;
attributes.Add(new ArrayPropertyInfo
{
ArrayProperty = att,
PropertyInfo = property,
DefaultDeserialization = property.GetCustomAttribute<JsonConversionAttribute>() != null,
JsonConverterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType
});
}
_typeAttributesCache.TryAdd(type, attributes);
return attributes;
}
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType)
{
if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception("1");
if (!_typeAttributesCache.TryGetValue(objectType, out var attributes))
attributes = CacheTypeAttributes(objectType);
int index = 0;
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
break;
var attribute = attributes.SingleOrDefault(a => a.ArrayProperty.Index == index);
var targetType = attribute.PropertyInfo.PropertyType;
object? value = null;
if (attribute.JsonConverterType != null)
{
// Has JsonConverter attribute
var options = new JsonSerializerOptions();
options.Converters.Add((JsonConverter)Activator.CreateInstance(attribute.JsonConverterType));
value = JsonDocument.ParseValue(ref reader).Deserialize(targetType, options);
}
else if (attribute.DefaultDeserialization)
{
// Use default deserialization
value = JsonDocument.ParseValue(ref reader).Deserialize(targetType);
}
else
{
value = reader.TokenType switch
{
JsonTokenType.Null => null,
JsonTokenType.False => false,
JsonTokenType.True => true,
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetDecimal(),
_ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"),
};
}
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, attribute.PropertyInfo.PropertyType, CultureInfo.InvariantCulture));
index++;
}
return result;
}
}
}
}
@@ -1,85 +0,0 @@
using System;
using System.Diagnostics;
using System.Runtime.Serialization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Bool converter
/// </summary>
public class BoolConverter : JsonConverterFactory
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert == typeof(bool) || typeToConvert == typeof(bool?);
}
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
Type converterType = typeof(BoolConverterInner<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType);
}
private class BoolConverterInner<T> : JsonConverter<T>
{
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> (T)((object?)ReadBool(ref reader, typeToConvert, options) ?? default(T))!;
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))
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null bool value, but property type is not a nullable bool");
return default;
}
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)
{
if (value is bool boolVal)
writer.WriteBooleanValue(boolVal);
else
writer.WriteNullValue();
}
}
}
}
@@ -1,202 +0,0 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Date time converter
/// </summary>
public class DateTimeConverter : JsonConverterFactory
{
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
private const double _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000d;
private const double _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000d / 1000;
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTime?);
}
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
Type converterType = typeof(DateTimeConverterInner<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType);
}
private class DateTimeConverterInner<T> : JsonConverter<T>
{
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> (T)((object?)ReadDateTime(ref reader, typeToConvert, options) ?? default(T))!;
private DateTime? ReadDateTime(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
if (typeToConvert == typeof(DateTime))
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | DateTime value of null, but property is not nullable");
return default;
}
if (reader.TokenType is JsonTokenType.Number)
{
var longValue = reader.GetDouble();
if (longValue == 0 || longValue == -1)
return default;
if (longValue < 19999999999)
return ConvertFromSeconds(longValue);
if (longValue < 19999999999999)
return ConvertFromMilliseconds(longValue);
if (longValue < 19999999999999999)
return ConvertFromMicroseconds(longValue);
return ConvertFromNanoseconds(longValue);
}
else if (reader.TokenType is JsonTokenType.String)
{
var stringValue = reader.GetString();
if (string.IsNullOrWhiteSpace(stringValue)
|| stringValue == "-1"
|| double.TryParse(stringValue, out var doubleVal) && doubleVal == 0)
{
return default;
}
if (stringValue!.Length == 8)
{
// Parse 20211103 format
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
{
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
return default;
}
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
}
if (stringValue.Length == 6)
{
// Parse 211103 format
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
{
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
return default;
}
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
}
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
{
// Parse 1637745563.000 format
if (doubleValue < 19999999999)
return ConvertFromSeconds(doubleValue);
if (doubleValue < 19999999999999)
return ConvertFromMilliseconds((long)doubleValue);
if (doubleValue < 19999999999999999)
return ConvertFromMicroseconds((long)doubleValue);
return ConvertFromNanoseconds((long)doubleValue);
}
if (stringValue.Length == 10)
{
// Parse 2021-11-03 format
var values = stringValue.Split('-');
if (!int.TryParse(values[0], out var year)
|| !int.TryParse(values[1], out var month)
|| !int.TryParse(values[2], out var day))
{
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
return default;
}
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
}
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
}
else
{
return reader.GetDateTime();
}
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
if (value == null)
writer.WriteNullValue();
else
{
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));
}
}
}
/// <summary>
/// Convert a seconds since epoch (01-01-1970) value to DateTime
/// </summary>
/// <param name="seconds"></param>
/// <returns></returns>
public static DateTime ConvertFromSeconds(double seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
/// <summary>
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
/// </summary>
/// <param name="milliseconds"></param>
/// <returns></returns>
public static DateTime ConvertFromMilliseconds(double milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
/// <summary>
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
/// </summary>
/// <param name="microseconds"></param>
/// <returns></returns>
public static DateTime ConvertFromMicroseconds(double microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
/// <param name="nanoseconds"></param>
/// <returns></returns>
public static DateTime ConvertFromNanoseconds(double nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
/// <summary>
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")]
public static long? ConvertToSeconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalSeconds);
/// <summary>
/// Convert a DateTime value to milliseconds since epoch (01-01-1970) value
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")]
public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds);
/// <summary>
/// Convert a DateTime value to microseconds since epoch (01-01-1970) value
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")]
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond);
/// <summary>
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")]
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
}
}
@@ -1,40 +0,0 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Decimal converter
/// </summary>
public class DecimalConverter : JsonConverter<decimal?>
{
/// <inheritdoc />
public override decimal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return null;
if (reader.TokenType == JsonTokenType.String)
{
var value = reader.GetString();
if (string.IsNullOrEmpty(value))
return null;
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
}
return reader.GetDecimal();
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, decimal? value, JsonSerializerOptions options)
{
if (value == null)
writer.WriteNullValue();
else
writer.WriteNumberValue(value.Value);
}
}
}
@@ -1,23 +0,0 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter for serializing decimal values as string
/// </summary>
public class DecimalStringWriterConverter : JsonConverter<decimal>
{
/// <inheritdoc />
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture) ?? null);
}
}
@@ -1,215 +0,0 @@
using CryptoExchange.Net.Attributes;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
/// </summary>
public class EnumConverter : JsonConverterFactory
{
private bool _warnOnMissingEntry = true;
private bool _writeAsInt;
private static readonly ConcurrentDictionary<Type, List<KeyValuePair<object, string>>> _mapping = new();
/// <summary>
/// </summary>
public EnumConverter() { }
/// <summary>
/// </summary>
/// <param name="writeAsInt"></param>
/// <param name="warnOnMissingEntry"></param>
public EnumConverter(bool writeAsInt, bool warnOnMissingEntry)
{
_writeAsInt = writeAsInt;
_warnOnMissingEntry = warnOnMissingEntry;
}
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsEnum || Nullable.GetUnderlyingType(typeToConvert)?.IsEnum == true;
}
/// <inheritdoc />
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
JsonConverter converter = (JsonConverter)Activator.CreateInstance(
typeof(EnumConverterInner<>).MakeGenericType(
new Type[] { typeToConvert }),
BindingFlags.Instance | BindingFlags.Public,
binder: null,
args: new object[] { _writeAsInt, _warnOnMissingEntry },
culture: null)!;
return converter;
}
private static List<KeyValuePair<object, string>> AddMapping(Type objectType)
{
var mapping = new List<KeyValuePair<object, string>>();
var enumMembers = objectType.GetMembers();
foreach (var member in enumMembers)
{
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
foreach (MapAttribute attribute in maps)
{
foreach (var value in attribute.Values)
mapping.Add(new KeyValuePair<object, string>(Enum.Parse(objectType, member.Name), value));
}
}
_mapping.TryAdd(objectType, mapping);
return mapping;
}
private class EnumConverterInner<T> : JsonConverter<T>
{
private bool _warnOnMissingEntry = true;
private bool _writeAsInt;
public EnumConverterInner(bool writeAsInt, bool warnOnMissingEntry)
{
_warnOnMissingEntry = warnOnMissingEntry;
_writeAsInt = writeAsInt;
}
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var enumType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert;
if (!_mapping.TryGetValue(enumType, out var mapping))
mapping = AddMapping(enumType);
var stringValue = reader.TokenType switch
{
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetInt16().ToString(),
JsonTokenType.True => reader.GetBoolean().ToString(),
JsonTokenType.False => reader.GetBoolean().ToString(),
JsonTokenType.Null => null,
_ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType)
};
if (string.IsNullOrEmpty(stringValue))
{
// Received null value
var emptyResult = GetDefaultValue(typeToConvert, enumType);
if (emptyResult != null)
// If the property we're parsing to isn't nullable there isn't a correct way to return this as null will either throw an exception (.net framework) or the default enum value (dotnet core).
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null enum value, but property type is not a nullable enum. EnumType: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo");
return (T?)emptyResult;
}
if (!GetValue(enumType, mapping, stringValue!, out var result))
{
var defaultValue = GetDefaultValue(typeToConvert, enumType);
if (string.IsNullOrWhiteSpace(stringValue))
{
if (defaultValue != null)
// We received an empty string and have no mapping for it, and the property isn't nullable
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received empty string as enum value, but property type is not a nullable enum. EnumType: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo");
}
else
{
// We received an enum value but weren't able to parse it.
if (_warnOnMissingEntry)
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {enumType.Name}, Value: {stringValue}, Known values: {string.Join(", ", mapping.Select(m => m.Value))}. If you think {stringValue} should added please open an issue on the Github repo");
}
return (T?)defaultValue;
}
return (T?)result;
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
if (value == null)
{
writer.WriteNullValue();
}
else
{
if (!_writeAsInt)
{
var stringValue = GetString(value.GetType(), value);
writer.WriteStringValue(stringValue);
}
else
{
writer.WriteNumberValue((int)Convert.ChangeType(value, typeof(int)));
}
}
}
private static object? GetDefaultValue(Type objectType, Type enumType)
{
if (Nullable.GetUnderlyingType(objectType) != null)
return null;
return Activator.CreateInstance(enumType); // return default value
}
private static bool GetValue(Type objectType, List<KeyValuePair<object, string>> enumMapping, string value, out object? result)
{
// Check for exact match first, then if not found fallback to a case insensitive match
var mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
if (mapping.Equals(default(KeyValuePair<object, string>)))
mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
if (!mapping.Equals(default(KeyValuePair<object, string>)))
{
result = mapping.Key;
return true;
}
try
{
// If no explicit mapping is found try to parse string
result = Enum.Parse(objectType, value, true);
return true;
}
catch (Exception)
{
result = default;
return false;
}
}
}
/// <summary>
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="enumValue"></param>
/// <returns></returns>
[return: NotNullIfNotNull("enumValue")]
public static string? GetString<T>(T enumValue) => GetString(typeof(T), enumValue);
/// <summary>
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
/// </summary>
/// <param name="objectType"></param>
/// <param name="enumValue"></param>
/// <returns></returns>
[return: NotNullIfNotNull("enumValue")]
public static string? GetString(Type objectType, object? enumValue)
{
objectType = Nullable.GetUnderlyingType(objectType) ?? objectType;
if (!_mapping.TryGetValue(objectType, out var mapping))
mapping = AddMapping(objectType);
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
}
}
}
@@ -1,27 +0,0 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Serializer options
/// </summary>
public static class SerializerOptions
{
/// <summary>
/// Json serializer settings which includes the EnumConverter, DateTimeConverter, BoolConverter and DecimalConverter
/// </summary>
public static JsonSerializerOptions WithConverters { get; } = new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
PropertyNameCaseInsensitive = false,
Converters =
{
new DateTimeConverter(),
new EnumConverter(),
new BoolConverter(),
new DecimalConverter(),
}
};
}
}
@@ -1,289 +0,0 @@
using CryptoExchange.Net.Converters.MessageParsing;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// System.Text.Json message accessor
/// </summary>
public abstract class SystemTextJsonMessageAccessor : IMessageAccessor
{
/// <summary>
/// The JsonDocument loaded
/// </summary>
protected JsonDocument? _document;
private static JsonSerializerOptions _serializerOptions = SerializerOptions.WithConverters;
/// <inheritdoc />
public bool IsJson { get; set; }
/// <inheritdoc />
public abstract bool OriginalDataAvailable { get; }
/// <inheritdoc />
public object? Underlying => throw new NotImplementedException();
/// <inheritdoc />
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
{
if (!IsJson)
return new CallResult<object>(GetOriginalString());
if (_document == null)
throw new InvalidOperationException("No json document loaded");
try
{
var result = _document.Deserialize(type, _serializerOptions);
return new CallResult<object>(result!);
}
catch (JsonException ex)
{
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
}
}
/// <inheritdoc />
public CallResult<T> Deserialize<T>(MessagePath? path = null)
{
if (_document == null)
throw new InvalidOperationException("No json document loaded");
try
{
var result = _document.Deserialize<T>(_serializerOptions);
return new CallResult<T>(result!);
}
catch (JsonException ex)
{
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
}
}
/// <inheritdoc />
public NodeType? GetNodeType()
{
if (!IsJson)
throw new InvalidOperationException("Can't access json data on non-json message");
if (_document == null)
throw new InvalidOperationException("No json document loaded");
return _document.RootElement.ValueKind switch
{
JsonValueKind.Object => NodeType.Object,
JsonValueKind.Array => NodeType.Array,
_ => NodeType.Value
};
}
/// <inheritdoc />
public NodeType? GetNodeType(MessagePath path)
{
if (!IsJson)
throw new InvalidOperationException("Can't access json data on non-json message");
var node = GetPathNode(path);
if (!node.HasValue)
return null;
return node.Value.ValueKind switch
{
JsonValueKind.Object => NodeType.Object,
JsonValueKind.Array => NodeType.Array,
_ => NodeType.Value
};
}
/// <inheritdoc />
public T? GetValue<T>(MessagePath path)
{
if (!IsJson)
throw new InvalidOperationException("Can't access json data on non-json message");
var value = GetPathNode(path);
if (value == null)
return default;
if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array)
return default;
return value.Value.Deserialize<T>();
}
/// <inheritdoc />
public List<T?>? GetValues<T>(MessagePath path) => throw new NotImplementedException();
private JsonElement? GetPathNode(MessagePath path)
{
if (!IsJson)
throw new InvalidOperationException("Can't access json data on non-json message");
if (_document == null)
throw new InvalidOperationException("No json document loaded");
JsonElement? currentToken = _document.RootElement;
foreach (var node in path)
{
if (node.Type == 0)
{
// Int value
var val = node.Index!.Value;
if (currentToken!.Value.ValueKind != JsonValueKind.Array || currentToken.Value.GetArrayLength() <= val)
return null;
currentToken = currentToken.Value[val];
}
else if (node.Type == 1)
{
// String value
if (currentToken!.Value.ValueKind != JsonValueKind.Object)
return null;
if (!currentToken.Value.TryGetProperty(node.Property!, out var token))
return null;
currentToken = token;
}
else
{
// Property name
if (currentToken!.Value.ValueKind != JsonValueKind.Object)
return null;
throw new NotImplementedException();
}
if (currentToken == null)
return null;
}
return currentToken;
}
/// <inheritdoc />
public abstract string GetOriginalString();
/// <inheritdoc />
public abstract void Clear();
}
/// <summary>
/// System.Text.Json stream message accessor
/// </summary>
public class SystemTextJsonStreamMessageAccessor : SystemTextJsonMessageAccessor, IStreamMessageAccessor
{
private Stream? _stream;
/// <inheritdoc />
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
/// <inheritdoc />
public async Task<bool> Read(Stream stream, bool bufferStream)
{
if (bufferStream && stream is not MemoryStream)
{
// We need to be buffer the stream, and it's not currently a seekable stream, so copy it to a new memory stream
_stream = new MemoryStream();
stream.CopyTo(_stream);
_stream.Position = 0;
}
else if (bufferStream)
{
// We need to buffer the stream, and the current stream is seekable, store as is
_stream = stream;
}
else
{
// We don't need to buffer the stream, so don't bother keeping the reference
}
try
{
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
IsJson = true;
}
catch (Exception)
{
// Not a json message
IsJson = false;
}
return IsJson;
}
/// <inheritdoc />
public override string GetOriginalString()
{
if (_stream is null)
throw new NullReferenceException("Stream not initialized");
_stream.Position = 0;
using var textReader = new StreamReader(_stream, Encoding.UTF8, false, 1024, true);
return textReader.ReadToEnd();
}
/// <inheritdoc />
public override void Clear()
{
_stream?.Dispose();
_stream = null;
_document = null;
}
}
/// <summary>
/// System.Text.Json byte message accessor
/// </summary>
public class SystemTextJsonByteMessageAccessor : SystemTextJsonMessageAccessor, IByteMessageAccessor
{
private ReadOnlyMemory<byte> _bytes;
/// <inheritdoc />
public bool Read(ReadOnlyMemory<byte> data)
{
_bytes = data;
try
{
_document = JsonDocument.Parse(data);
IsJson = true;
}
catch (Exception)
{
// Not a json message
IsJson = false;
}
return IsJson;
}
/// <inheritdoc />
public override string GetOriginalString() =>
// Netstandard 2.0 doesn't support GetString from a ReadonlySpan<byte>, so use ToArray there instead
#if NETSTANDARD2_0
Encoding.UTF8.GetString(_bytes.ToArray());
#else
Encoding.UTF8.GetString(_bytes.Span);
#endif
/// <inheritdoc />
public override bool OriginalDataAvailable => true;
/// <inheritdoc />
public override void Clear()
{
_bytes = null;
_document = null;
}
}
}
@@ -1,12 +0,0 @@
using CryptoExchange.Net.Interfaces;
using System.Text.Json;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <inheritdoc />
public class SystemTextJsonMessageSerializer : IMessageSerializer
{
/// <inheritdoc />
public string Serialize(object message) => JsonSerializer.Serialize(message, SerializerOptions.WithConverters);
}
}
+10 -18
View File
@@ -5,28 +5,21 @@
<PropertyGroup>
<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>7.2.1</PackageVersion>
<AssemblyVersion>7.2.1</AssemblyVersion>
<FileVersion>7.2.1</FileVersion>
<Description>A base package for implementing cryptocurrency API's</Description>
<PackageVersion>7.0.0-beta1</PackageVersion>
<AssemblyVersion>7.0.0-beta1</AssemblyVersion>
<FileVersion>7.0.0-beta1</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</PackageTags>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
<NeutralLanguage>en</NeutralLanguage>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>icon.png</PackageIcon>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
<PackageReleaseNotes>7.0.0-beta1 - Full overhaul of Websocket message handling, Abstracted out Newtonsoft.Json references in preparation of moving to System.Text.Json, Updated SendPeriodic to operate on connection level instead of client level to prevent looping when there are no connections, Added check to not send an unsubscribe message if there is another subscription listening to the same events, Added CryptoRestClient and CryptoSocketClient as aggregate for accessing different exchange APIs, Updated socket client log messages, Updated socket client GetSubscriptionState output</PackageReleaseNotes>
<Nullable>enable</Nullable>
<LangVersion>10.0</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<None Include="Icon\icon.png" Pack="true" PackagePath="\" />
<None Include="..\README.md" Pack="true" PackagePath="\" />
</ItemGroup>
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
@@ -44,7 +37,7 @@
<DocumentationFile>CryptoExchange.Net.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0.1">
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -52,12 +45,11 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.32" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageReference Include="System.Text.Json" Version="8.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.32" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.32" />
</ItemGroup>
</Project>
+83 -72
View File
@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO.Compression;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security;
@@ -10,10 +8,7 @@ using System.Text;
using System.Web;
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
using System.Globalization;
using System.Collections;
using System.Net.Http;
using System.Data.Common;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net
@@ -34,6 +29,18 @@ namespace CryptoExchange.Net
parameters.Add(key, value);
}
/// <summary>
/// Add a parameter
/// </summary>
/// <param name="parameters"></param>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="converter"></param>
public static void AddParameter(this Dictionary<string, object> parameters, string key, string value, JsonConverter converter)
{
parameters.Add(key, JsonConvert.SerializeObject(value, converter));
}
/// <summary>
/// Add a parameter
/// </summary>
@@ -45,6 +52,18 @@ namespace CryptoExchange.Net
parameters.Add(key, value);
}
/// <summary>
/// Add a parameter
/// </summary>
/// <param name="parameters"></param>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="converter"></param>
public static void AddParameter(this Dictionary<string, object> parameters, string key, object value, JsonConverter converter)
{
parameters.Add(key, JsonConvert.SerializeObject(value, converter));
}
/// <summary>
/// Add an optional parameter. Not added if value is null
/// </summary>
@@ -57,6 +76,19 @@ namespace CryptoExchange.Net
parameters.Add(key, value);
}
/// <summary>
/// Add an optional parameter. Not added if value is null
/// </summary>
/// <param name="parameters"></param>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="converter"></param>
public static void AddOptionalParameter(this Dictionary<string, object> parameters, string key, object? value, JsonConverter converter)
{
if (value != null)
parameters.Add(key, JsonConvert.SerializeObject(value, converter));
}
/// <summary>
/// Create a query string of the specified parameters
/// </summary>
@@ -64,7 +96,7 @@ namespace CryptoExchange.Net
/// <param name="urlEncodeValues">Whether or not the values should be url encoded</param>
/// <param name="serializationType">How to serialize array parameters</param>
/// <returns></returns>
public static string CreateParamString(this IDictionary<string, object> parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType)
public static string CreateParamString(this Dictionary<string, object> parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType)
{
var uriString = string.Empty;
var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList();
@@ -72,22 +104,17 @@ namespace CryptoExchange.Net
{
if (serializationType == ArrayParametersSerialization.Array)
{
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={string.Format(CultureInfo.InvariantCulture, "{0}", v)}"))}&";
}
else if (serializationType == ArrayParametersSerialization.MultipleValues)
{
var array = (Array)arrayEntry.Value;
uriString += string.Join("&", array.OfType<object>().Select(a => $"{arrayEntry.Key}={Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", a))}"));
uriString += "&";
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={v}"))}&";
}
else
{
var array = (Array)arrayEntry.Value;
uriString += $"{arrayEntry.Key}=[{string.Join(",", array.OfType<object>().Select(a => string.Format(CultureInfo.InvariantCulture, "{0}", a)))}]&";
uriString += string.Join("&", array.OfType<object>().Select(a => $"{arrayEntry.Key}={Uri.EscapeDataString(a.ToString())}"));
uriString += "&";
}
}
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 += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={(urlEncodeValues ? Uri.EscapeDataString(s.Value.ToString()) : s.Value)}"))}";
uriString = uriString.TrimEnd('&');
return uriString;
}
@@ -97,7 +124,7 @@ namespace CryptoExchange.Net
/// </summary>
/// <param name="parameters"></param>
/// <returns></returns>
public static string ToFormData(this IDictionary<string, object> parameters)
public static string ToFormData(this SortedDictionary<string, object> parameters)
{
var formData = HttpUtility.ParseQueryString(string.Empty);
foreach (var kvp in parameters)
@@ -106,15 +133,16 @@ namespace CryptoExchange.Net
{
var array = (Array)kvp.Value;
foreach (var value in array)
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", value));
formData.Add(kvp.Key, value.ToString());
}
else
{
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value));
formData.Add(kvp.Key, kvp.Value.ToString());
}
}
return formData.ToString();
}
/// <summary>
/// Get the string the secure string is representing
@@ -202,6 +230,37 @@ namespace CryptoExchange.Net
return secureString;
}
/// <summary>
/// String to JToken
/// </summary>
/// <param name="stringData"></param>
/// <param name="logger"></param>
/// <returns></returns>
public static JToken? ToJToken(this string stringData, ILogger? logger = null)
{
if (string.IsNullOrEmpty(stringData))
return null;
try
{
return JToken.Parse(stringData);
}
catch (JsonReaderException jre)
{
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}. Data: {stringData}";
logger?.Log(LogLevel.Error, info);
if (logger == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
return null;
}
catch (JsonSerializationException jse)
{
var info = $"Deserialize JsonSerializationException: {jse.Message}. Data: {stringData}";
logger?.Log(LogLevel.Error, info);
if (logger == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
return null;
}
}
/// <summary>
/// Validates an int is one of the allowed values
/// </summary>
@@ -358,26 +417,10 @@ namespace CryptoExchange.Net
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
foreach (var parameter in parameters)
{
if (parameter.Value.GetType().IsArray)
if(parameter.Value.GetType().IsArray)
{
if (arraySerialization == ArrayParametersSerialization.JsonArray)
{
httpValueCollection.Add(parameter.Key, $"[{string.Join(",", (object[])parameter.Value)}]");
}
else
{
foreach (var item in (object[])parameter.Value)
{
if (arraySerialization == ArrayParametersSerialization.Array)
{
httpValueCollection.Add(parameter.Key + "[]", item.ToString());
}
else
{
httpValueCollection.Add(parameter.Key, item.ToString());
}
}
}
foreach (var item in (object[])parameter.Value)
httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString());
}
else
{
@@ -407,24 +450,8 @@ namespace CryptoExchange.Net
{
if (parameter.Value.GetType().IsArray)
{
if (arraySerialization == ArrayParametersSerialization.JsonArray)
{
httpValueCollection.Add(parameter.Key, $"[{string.Join(",", (object[])parameter.Value)}]");
}
else
{
foreach (var item in (object[])parameter.Value)
{
if (arraySerialization == ArrayParametersSerialization.Array)
{
httpValueCollection.Add(parameter.Key + "[]", item.ToString());
}
else
{
httpValueCollection.Add(parameter.Key, item.ToString());
}
}
}
foreach (var item in (object[])parameter.Value)
httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString());
}
else
{
@@ -454,22 +481,6 @@ namespace CryptoExchange.Net
return ub.Uri;
}
/// <summary>
/// Decompress using Gzip
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
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);
deflateStream.CopyTo(decompressedStream);
return new ReadOnlyMemory<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length);
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

@@ -1,6 +1,7 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
@@ -17,10 +18,6 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
public int Id { get; }
/// <summary>
/// Whether this listener can handle data
/// </summary>
public bool CanHandleData { get; }
/// <summary>
/// The identifiers for this processor
/// </summary>
public HashSet<string> ListenerIdentifiers { get; }
@@ -30,7 +27,7 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="connection"></param>
/// <param name="message"></param>
/// <returns></returns>
CallResult Handle(SocketConnection connection, DataEvent<object> message);
Task<CallResult> HandleAsync(SocketConnection connection, DataEvent<object> message);
/// <summary>
/// Get the type the message should be deserialized to
/// </summary>
@@ -38,11 +35,11 @@ namespace CryptoExchange.Net.Interfaces
/// <returns></returns>
Type? GetMessageType(IMessageAccessor messageAccessor);
/// <summary>
/// Deserialize a message into object of type
/// Deserialize a message int oobject of type
/// </summary>
/// <param name="accessor"></param>
/// <param name="type"></param>
/// <returns></returns>
CallResult<object> Deserialize(IMessageAccessor accessor, Type type);
object Deserialize(IMessageAccessor accessor, Type type);
}
}
@@ -17,10 +17,5 @@ namespace CryptoExchange.Net.Interfaces
/// The total amount of requests made with this client
/// </summary>
int TotalRequestsMade { get; }
/// <summary>
/// The exchange name
/// </summary>
string Exchange { get; }
}
}
@@ -36,7 +36,7 @@ namespace CryptoExchange.Net.Interfaces
/// <summary>
/// Log the current state of connections and subscriptions
/// </summary>
string GetSubscriptionsState(bool includeSubDetails = true);
string GetSubscriptionsState();
/// <summary>
/// Reconnect all connections
/// </summary>
@@ -10,11 +10,6 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
public interface ISocketClient: IDisposable
{
/// <summary>
/// The exchange name
/// </summary>
string Exchange { get; }
/// <summary>
/// The options provided for this client
/// </summary>
@@ -12,14 +12,9 @@ namespace CryptoExchange.Net.Interfaces
public interface ISymbolOrderBook
{
/// <summary>
/// The exchange the book is for
/// Identifier
/// </summary>
string Exchange { get; }
/// <summary>
/// The Api the book is for
/// </summary>
string Api { get; }
string Id { get; }
/// <summary>
/// The status of the order book. Order book is up to date when the status is `Synced`
+1 -1
View File
@@ -17,7 +17,7 @@ namespace CryptoExchange.Net.Interfaces
/// <summary>
/// Websocket message received event
/// </summary>
event Action<WebSocketMessageType, ReadOnlyMemory<byte>> OnStreamMessage;
event Func<WebSocketMessageType, Stream, Task> OnStreamMessage;
/// <summary>
/// Websocket sent event, RequestId as parameter
/// </summary>
@@ -1,348 +0,0 @@
using Microsoft.Extensions.Logging;
using System;
namespace CryptoExchange.Net.Logging.Extensions
{
internal static class CryptoExchangeWebSocketClientLoggingExtension
{
private static readonly Action<ILogger, int, Exception?> _connecting;
private static readonly Action<ILogger, int, string, Exception?> _connectionFailed;
private static readonly Action<ILogger, int, Uri, Exception?> _connected;
private static readonly Action<ILogger, int, Exception?> _startingProcessing;
private static readonly Action<ILogger, int, Exception?> _finishedProcessing;
private static readonly Action<ILogger, int, Exception?> _attemptReconnect;
private static readonly Action<ILogger, int, Uri, Exception?> _setReconnectUri;
private static readonly Action<ILogger, int, int, int, Exception?> _addingBytesToSendBuffer;
private static readonly Action<ILogger, int, Exception?> _reconnectRequested;
private static readonly Action<ILogger, int, Exception?> _closeAsyncWaitingForExistingCloseTask;
private static readonly Action<ILogger, int, Exception?> _closeAsyncSocketNotOpen;
private static readonly Action<ILogger, int, Exception?> _closing;
private static readonly Action<ILogger, int, Exception?> _closed;
private static readonly Action<ILogger, int, Exception?> _disposing;
private static readonly Action<ILogger, int, Exception?> _disposed;
private static readonly Action<ILogger, int, int, int, Exception?> _sendDelayedBecauseOfRateLimit;
private static readonly Action<ILogger, int, int, int, Exception?> _sentBytes;
private static readonly Action<ILogger, int, string, Exception?> _sendLoopStoppedWithException;
private static readonly Action<ILogger, int, Exception?> _sendLoopFinished;
private static readonly Action<ILogger, int, string, string ,Exception?> _receivedCloseMessage;
private static readonly Action<ILogger, int, int, Exception?> _receivedPartialMessage;
private static readonly Action<ILogger, int, int, Exception?> _receivedSingleMessage;
private static readonly Action<ILogger, int, long, Exception?> _reassembledMessage;
private static readonly Action<ILogger, int, long, Exception?> _discardIncompleteMessage;
private static readonly Action<ILogger, int, Exception?> _receiveLoopStoppedWithException;
private static readonly Action<ILogger, int, Exception?> _receiveLoopFinished;
private static readonly Action<ILogger, int, TimeSpan?, Exception?> _startingTaskForNoDataReceivedCheck;
private static readonly Action<ILogger, int, TimeSpan?, Exception?> _noDataReceiveTimoutReconnect;
static CryptoExchangeWebSocketClientLoggingExtension()
{
_connecting = LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(1000, "Connecting"),
"[Sckt {SocketId}] connecting");
_connectionFailed = LoggerMessage.Define<int, string>(
LogLevel.Error,
new EventId(1001, "ConnectionFailed"),
"[Sckt {SocketId}] connection failed: {ErrorMessage}");
_connected = LoggerMessage.Define<int, Uri?>(
LogLevel.Debug,
new EventId(1002, "Connected"),
"[Sckt {SocketId}] connected to {Uri}");
_startingProcessing = LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(1003, "StartingProcessing"),
"[Sckt {SocketId}] starting processing tasks");
_finishedProcessing = LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(1004, "FinishedProcessing"),
"[Sckt {SocketId}] processing tasks finished");
_attemptReconnect = LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(1005, "AttemptReconnect"),
"[Sckt {SocketId}] attempting to reconnect");
_setReconnectUri = LoggerMessage.Define<int, Uri>(
LogLevel.Debug,
new EventId(1006, "SetReconnectUri"),
"[Sckt {SocketId}] reconnect URI set to {ReconnectUri}");
_addingBytesToSendBuffer = LoggerMessage.Define<int, int, int>(
LogLevel.Trace,
new EventId(1007, "AddingBytesToSendBuffer"),
"[Sckt {SocketId}] msg {RequestId} - Adding {NumBytes} bytes to send buffer");
_reconnectRequested = LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(1008, "ReconnectRequested"),
"[Sckt {SocketId}] reconnect requested");
_closeAsyncWaitingForExistingCloseTask = LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(1009, "CloseAsyncWaitForExistingCloseTask"),
"[Sckt {SocketId}] CloseAsync() waiting for existing close task");
_closeAsyncSocketNotOpen = LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(1010, "CloseAsyncSocketNotOpen"),
"[Sckt {SocketId}] CloseAsync() socket not open");
_closing = LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(1011, "Closing"),
"[Sckt {SocketId}] closing");
_closed = LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(1012, "Closed"),
"[Sckt {SocketId}] closed");
_disposing = LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(1013, "Disposing"),
"[Sckt {SocketId}] disposing");
_disposed = LoggerMessage.Define<int>(
LogLevel.Trace,
new EventId(1014, "Disposed"),
"[Sckt {SocketId}] disposed");
_sendDelayedBecauseOfRateLimit = LoggerMessage.Define<int, int, int>(
LogLevel.Debug,
new EventId(1015, "SendDelayedBecauseOfRateLimit"),
"[Sckt {SocketId}] msg {RequestId} - send delayed {DelayMS}ms because of rate limit");
_sentBytes = LoggerMessage.Define<int, int, int>(
LogLevel.Trace,
new EventId(1016, "SentBytes"),
"[Sckt {SocketId}] msg {RequestId} - sent {NumBytes} bytes");
_sendLoopStoppedWithException = LoggerMessage.Define<int, string>(
LogLevel.Warning,
new EventId(1017, "SendLoopStoppedWithException"),
"[Sckt {SocketId}] send loop stopped with exception: {ErrorMessage}");
_sendLoopFinished = LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(1018, "SendLoopFinished"),
"[Sckt {SocketId}] send loop finished");
_receivedCloseMessage = LoggerMessage.Define<int, string, string>(
LogLevel.Debug,
new EventId(1019, "ReceivedCloseMessage"),
"[Sckt {SocketId}] received `Close` message, CloseStatus: {CloseStatus}, CloseStatusDescription: {CloseStatusDescription}");
_receivedPartialMessage = LoggerMessage.Define<int, int>(
LogLevel.Trace,
new EventId(1020, "ReceivedPartialMessage"),
"[Sckt {SocketId}] received {NumBytes} bytes in partial message");
_receivedSingleMessage = LoggerMessage.Define<int, int>(
LogLevel.Trace,
new EventId(1021, "ReceivedSingleMessage"),
"[Sckt {SocketId}] received {NumBytes} bytes in single message");
_reassembledMessage = LoggerMessage.Define<int, long>(
LogLevel.Trace,
new EventId(1022, "ReassembledMessage"),
"[Sckt {SocketId}] reassembled message of {NumBytes} bytes");
_discardIncompleteMessage = LoggerMessage.Define<int, long>(
LogLevel.Trace,
new EventId(1023, "DiscardIncompleteMessage"),
"[Sckt {SocketId}] discarding incomplete message of {NumBytes} bytes");
_receiveLoopStoppedWithException = LoggerMessage.Define<int>(
LogLevel.Warning,
new EventId(1024, "ReceiveLoopStoppedWithException"),
"[Sckt {SocketId}] receive loop stopped with exception");
_receiveLoopFinished = LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(1025, "ReceiveLoopFinished"),
"[Sckt {SocketId}] receive loop finished");
_startingTaskForNoDataReceivedCheck = LoggerMessage.Define<int, TimeSpan?>(
LogLevel.Debug,
new EventId(1026, "StartingTaskForNoDataReceivedCheck"),
"[Sckt {SocketId}] starting task checking for no data received for {Timeout}");
_noDataReceiveTimoutReconnect = LoggerMessage.Define<int, TimeSpan?>(
LogLevel.Debug,
new EventId(1027, "NoDataReceiveTimeoutReconnect"),
"[Sckt {SocketId}] no data received for {Timeout}, reconnecting socket");
}
public static void SocketConnecting(
this ILogger logger, int socketId)
{
_connecting(logger, socketId, null);
}
public static void SocketConnectionFailed(
this ILogger logger, int socketId, string message, Exception e)
{
_connectionFailed(logger, socketId, message, e);
}
public static void SocketConnected(
this ILogger logger, int socketId, Uri uri)
{
_connected(logger, socketId, uri, null);
}
public static void SocketStartingProcessing(
this ILogger logger, int socketId)
{
_startingProcessing(logger, socketId, null);
}
public static void SocketFinishedProcessing(
this ILogger logger, int socketId)
{
_finishedProcessing(logger, socketId, null);
}
public static void SocketAttemptReconnect(
this ILogger logger, int socketId)
{
_attemptReconnect(logger, socketId, null);
}
public static void SocketSetReconnectUri(
this ILogger logger, int socketId, Uri uri)
{
_setReconnectUri(logger, socketId, uri, null);
}
public static void SocketAddingBytesToSendBuffer(
this ILogger logger, int socketId, int requestId, byte[] bytes)
{
_addingBytesToSendBuffer(logger, socketId, requestId, bytes.Length, null);
}
public static void SocketReconnectRequested(
this ILogger logger, int socketId)
{
_reconnectRequested(logger, socketId, null);
}
public static void SocketCloseAsyncWaitingForExistingCloseTask(
this ILogger logger, int socketId)
{
_closeAsyncWaitingForExistingCloseTask(logger, socketId, null);
}
public static void SocketCloseAsyncSocketNotOpen(
this ILogger logger, int socketId)
{
_closeAsyncSocketNotOpen(logger, socketId, null);
}
public static void SocketClosing(
this ILogger logger, int socketId)
{
_closing(logger, socketId, null);
}
public static void SocketClosed(
this ILogger logger, int socketId)
{
_closed(logger, socketId, null);
}
public static void SocketDisposing(
this ILogger logger, int socketId)
{
_disposing(logger, socketId, null);
}
public static void SocketDisposed(
this ILogger logger, int socketId)
{
_disposed(logger, socketId, null);
}
public static void SocketSendDelayedBecauseOfRateLimit(
this ILogger logger, int socketId, int requestId, int delay)
{
_sendDelayedBecauseOfRateLimit(logger, socketId, requestId, delay, null);
}
public static void SocketSentBytes(
this ILogger logger, int socketId, int requestId, int numBytes)
{
_sentBytes(logger, socketId, requestId, numBytes, null);
}
public static void SocketSendLoopStoppedWithException(
this ILogger logger, int socketId, string message, Exception e)
{
_sendLoopStoppedWithException(logger, socketId, message, e);
}
public static void SocketSendLoopFinished(
this ILogger logger, int socketId)
{
_sendLoopFinished(logger, socketId, null);
}
public static void SocketReceivedCloseMessage(
this ILogger logger, int socketId, string webSocketCloseStatus, string closeStatusDescription)
{
_receivedCloseMessage(logger, socketId, webSocketCloseStatus, closeStatusDescription, null);
}
public static void SocketReceivedPartialMessage(
this ILogger logger, int socketId, int countBytes)
{
_receivedPartialMessage(logger, socketId, countBytes, null);
}
public static void SocketReceivedSingleMessage(
this ILogger logger, int socketId, int countBytes)
{
_receivedSingleMessage(logger, socketId, countBytes, null);
}
public static void SocketReassembledMessage(
this ILogger logger, int socketId, long countBytes)
{
_reassembledMessage(logger, socketId, countBytes, null);
}
public static void SocketDiscardIncompleteMessage(
this ILogger logger, int socketId, long countBytes)
{
_discardIncompleteMessage(logger, socketId, countBytes, null);
}
public static void SocketReceiveLoopStoppedWithException(
this ILogger logger, int socketId, Exception e)
{
_receiveLoopStoppedWithException(logger, socketId, e);
}
public static void SocketReceiveLoopFinished(
this ILogger logger, int socketId)
{
_receiveLoopFinished(logger, socketId, null);
}
public static void SocketStartingTaskForNoDataReceivedCheck(
this ILogger logger, int socketId, TimeSpan? timeSpan)
{
_startingTaskForNoDataReceivedCheck(logger, socketId, timeSpan, null);
}
public static void SocketNoDataReceiveTimoutReconnect(
this ILogger logger, int socketId, TimeSpan? timeSpan)
{
_noDataReceiveTimoutReconnect(logger, socketId, timeSpan, null);
}
}
}
@@ -1,81 +0,0 @@
using Microsoft.Extensions.Logging;
using System;
using System.Net;
using System.Net.Http;
namespace CryptoExchange.Net.Logging.Extensions
{
internal static class RestApiClientLoggingExtensions
{
private static readonly Action<ILogger, int?, int?, long, string?, Exception?> _restApiErrorReceived;
private static readonly Action<ILogger, int?, int?, long, string?, Exception?> _restApiResponseReceived;
private static readonly Action<ILogger, int, string, Exception?> _restApiFailedToSyncTime;
private static readonly Action<ILogger, int, string, Exception?> _restApiNoApiCredentials;
private static readonly Action<ILogger, int, Uri, Exception?> _restApiCreatingRequest;
private static readonly Action<ILogger, int, HttpMethod, string, Uri, string, Exception?> _restApiSendingRequest;
static RestApiClientLoggingExtensions()
{
_restApiErrorReceived = LoggerMessage.Define<int?, int?, long, string?>(
LogLevel.Warning,
new EventId(4000, "RestApiErrorReceived"),
"[Req {RequestId}] {ResponseStatusCode} - Error received in {ResponseTime}ms: {ErrorMessage}");
_restApiResponseReceived = LoggerMessage.Define<int?, int?, long, string?>(
LogLevel.Debug,
new EventId(4001, "RestApiResponseReceived"),
"[Req {RequestId}] {ResponseStatusCode} - Response received in {ResponseTime}ms: {OriginalData}");
_restApiFailedToSyncTime = LoggerMessage.Define<int, string>(
LogLevel.Debug,
new EventId(4002, "RestApifailedToSyncTime"),
"[Req {RequestId}] Failed to sync time, aborting request: {ErrorMessage}");
_restApiNoApiCredentials = LoggerMessage.Define<int, string>(
LogLevel.Warning,
new EventId(4003, "RestApiNoApiCredentials"),
"[Req {RequestId}] Request {RestApiUri} failed because no ApiCredentials were provided");
_restApiCreatingRequest = LoggerMessage.Define<int, Uri>(
LogLevel.Information,
new EventId(4004, "RestApiCreatingRequest"),
"[Req {RequestId}] Creating request for {RestApiUri}");
_restApiSendingRequest = LoggerMessage.Define<int, HttpMethod, string, Uri, string>(
LogLevel.Trace,
new EventId(4005, "RestApiSendingRequest"),
"[Req {RequestId}] Sending {Method} {Signed} request to {RestApiUri}{Query}");
}
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error)
{
_restApiErrorReceived(logger, requestId, (int?)responseStatusCode, responseTime, error, null);
}
public static void RestApiResponseReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? originalData)
{
_restApiResponseReceived(logger, requestId, (int?)responseStatusCode, responseTime, originalData, null);
}
public static void RestApiFailedToSyncTime(this ILogger logger, int requestId, string error)
{
_restApiFailedToSyncTime(logger, requestId, error, null);
}
public static void RestApiNoApiCredentials(this ILogger logger, int requestId, string uri)
{
_restApiNoApiCredentials(logger, requestId, uri, null);
}
public static void RestApiCreatingRequest(this ILogger logger, int requestId, Uri uri)
{
_restApiCreatingRequest(logger, requestId, uri, null);
}
public static void RestApiSendingRequest(this ILogger logger, int requestId, HttpMethod method, string signed, Uri uri, string paramString)
{
_restApiSendingRequest(logger, requestId, method, signed, uri, paramString, null);
}
}
}
@@ -1,188 +0,0 @@
using Microsoft.Extensions.Logging;
using System;
namespace CryptoExchange.Net.Logging.Extensions
{
internal static class SocketApiClientLoggingExtension
{
private static readonly Action<ILogger, int, Exception?> _failedToAddSubscriptionRetryOnDifferentConnection;
private static readonly Action<ILogger, int, Exception?> _hasBeenPausedCantSubscribeAtThisMoment;
private static readonly Action<ILogger, int, string?, Exception?> _failedToSubscribe;
private static readonly Action<ILogger, int, int, Exception?> _cancellationTokenSetClosingSubscription;
private static readonly Action<ILogger, int, int, Exception?> _subscriptionCompletedSuccessfully;
private static readonly Action<ILogger, int, Exception?> _hasBeenPausedCantSendQueryAtThisMoment;
private static readonly Action<ILogger, int, Exception?> _attemptingToAuthenticate;
private static readonly Action<ILogger, int, Exception?> _authenticationFailed;
private static readonly Action<ILogger, int, Exception?> _authenticated;
private static readonly Action<ILogger, string?, Exception?> _failedToDetermineConnectionUrl;
private static readonly Action<ILogger, string, Exception?> _connectionAddressSetTo;
private static readonly Action<ILogger, int, string, Exception?> _socketCreatedForAddress;
private static readonly Action<ILogger, int, Exception?> _unsubscribingAll;
private static readonly Action<ILogger, Exception?> _disposingSocketClient;
private static readonly Action<ILogger, int, int, Exception?> _unsubscribingSubscription;
private static readonly Action<ILogger, int, Exception?> _reconnectingAllConnections;
static SocketApiClientLoggingExtension()
{
_failedToAddSubscriptionRetryOnDifferentConnection = LoggerMessage.Define<int>(
LogLevel.Trace,
new EventId(3000, "FailedToAddSubscriptionRetryOnDifferentConnection"),
"[Sckt {SocketId}] failed to add subscription, retrying on different connection");
_hasBeenPausedCantSubscribeAtThisMoment = LoggerMessage.Define<int>(
LogLevel.Warning,
new EventId(3001, "HasBeenPausedCantSubscribeAtThisMoment"),
"[Sckt {SocketId}] has been paused, can't subscribe at this moment");
_failedToSubscribe = LoggerMessage.Define<int, string?>(
LogLevel.Warning,
new EventId(3002, "FailedToSubscribe"),
"[Sckt {SocketId}] failed to subscribe: {ErrorMessage}");
_cancellationTokenSetClosingSubscription = LoggerMessage.Define<int, int>(
LogLevel.Information,
new EventId(3003, "CancellationTokenSetClosingSubscription"),
"[Sckt {SocketId}] Cancellation token set, closing subscription {SubscriptionId}");
_subscriptionCompletedSuccessfully = LoggerMessage.Define<int, int>(
LogLevel.Information,
new EventId(3004, "SubscriptionCompletedSuccessfully"),
"[Sckt {SocketId}] subscription {SubscriptionId} completed successfully");
_hasBeenPausedCantSendQueryAtThisMoment = LoggerMessage.Define<int>(
LogLevel.Warning,
new EventId(3005, "HasBeenPausedCantSendQueryAtThisMoment"),
"[Sckt {SocketId}] has been paused, can't send query at this moment");
_attemptingToAuthenticate = LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(3006, "AttemptingToAuthenticate"),
"[Sckt {SocketId}] Attempting to authenticate");
_authenticationFailed = LoggerMessage.Define<int>(
LogLevel.Warning,
new EventId(3007, "AuthenticationFailed"),
"[Sckt {SocketId}] authentication failed");
_authenticated = LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(3008, "Authenticated"),
"[Sckt {SocketId}] authenticated");
_failedToDetermineConnectionUrl = LoggerMessage.Define<string?>(
LogLevel.Warning,
new EventId(3009, "FailedToDetermineConnectionUrl"),
"Failed to determine connection url: {ErrorMessage}");
_connectionAddressSetTo = LoggerMessage.Define<string>(
LogLevel.Debug,
new EventId(3010, "ConnectionAddressSetTo"),
"Connection address set to {ConnectionAddress}");
_socketCreatedForAddress = LoggerMessage.Define<int, string>(
LogLevel.Debug,
new EventId(3011, "SocketCreatedForAddress"),
"[Sckt {SocketId}] created for {Address}");
_unsubscribingAll = LoggerMessage.Define<int>(
LogLevel.Information,
new EventId(3013, "UnsubscribingAll"),
"Unsubscribing all {SubscriptionCount} subscriptions");
_disposingSocketClient = LoggerMessage.Define(
LogLevel.Debug,
new EventId(3015, "DisposingSocketClient"),
"Disposing socket client, closing all subscriptions");
_unsubscribingSubscription = LoggerMessage.Define<int, int>(
LogLevel.Information,
new EventId(3016, "UnsubscribingSubscription"),
"[Sckt {SocketId}] Unsubscribing subscription {SubscriptionId}");
_reconnectingAllConnections = LoggerMessage.Define<int>(
LogLevel.Information,
new EventId(3017, "ReconnectingAll"),
"Reconnecting all {ConnectionCount} connections");
}
public static void FailedToAddSubscriptionRetryOnDifferentConnection(this ILogger logger, int socketId)
{
_failedToAddSubscriptionRetryOnDifferentConnection(logger, socketId, null);
}
public static void HasBeenPausedCantSubscribeAtThisMoment(this ILogger logger, int socketId)
{
_hasBeenPausedCantSubscribeAtThisMoment(logger, socketId, null);
}
public static void FailedToSubscribe(this ILogger logger, int socketId, string? error)
{
_failedToSubscribe(logger, socketId, error, null);
}
public static void CancellationTokenSetClosingSubscription(this ILogger logger, int socketId, int subscriptionId)
{
_cancellationTokenSetClosingSubscription(logger, socketId, subscriptionId, null);
}
public static void SubscriptionCompletedSuccessfully(this ILogger logger, int socketId, int subscriptionId)
{
_subscriptionCompletedSuccessfully(logger, socketId, subscriptionId, null);
}
public static void HasBeenPausedCantSendQueryAtThisMoment(this ILogger logger, int socketId)
{
_hasBeenPausedCantSendQueryAtThisMoment(logger, socketId, null);
}
public static void AttemptingToAuthenticate(this ILogger logger, int socketId)
{
_attemptingToAuthenticate(logger, socketId, null);
}
public static void AuthenticationFailed(this ILogger logger, int socketId)
{
_authenticationFailed(logger, socketId, null);
}
public static void Authenticated(this ILogger logger, int socketId)
{
_authenticated(logger, socketId, null);
}
public static void FailedToDetermineConnectionUrl(this ILogger logger, string? error)
{
_failedToDetermineConnectionUrl(logger, error, null);
}
public static void ConnectionAddressSetTo(this ILogger logger, string connectionAddress)
{
_connectionAddressSetTo(logger, connectionAddress, null);
}
public static void SocketCreatedForAddress(this ILogger logger, int socketId, string address)
{
_socketCreatedForAddress(logger, socketId, address, null);
}
public static void UnsubscribingAll(this ILogger logger, int subscriptionCount)
{
_unsubscribingAll(logger, subscriptionCount, null);
}
public static void DisposingSocketClient(this ILogger logger)
{
_disposingSocketClient(logger, null);
}
public static void UnsubscribingSubscription(this ILogger logger, int socketId, int subscriptionId)
{
_unsubscribingSubscription(logger, socketId, subscriptionId, null);
}
public static void ReconnectingAllConnections(this ILogger logger, int connectionCount)
{
_reconnectingAllConnections(logger, connectionCount, null);
}
}
}
@@ -1,325 +0,0 @@
using System;
using System.Net.WebSockets;
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Logging.Extensions
{
internal 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, string?, Exception?> _failedReconnectProcessing;
private static readonly Action<ILogger, int, Exception?> _unkownExceptionWhileProcessingReconnection;
private static readonly Action<ILogger, int, WebSocketError, string?, Exception?> _webSocketErrorCodeAndDetails;
private static readonly Action<ILogger, int, string?, Exception?> _webSocketError;
private static readonly Action<ILogger, int, int, Exception?> _messageSentNotPending;
private static readonly Action<ILogger, int, string, Exception?> _receivedData;
private static readonly Action<ILogger, int, string, Exception?> _failedToEvaluateMessage;
private static readonly Action<ILogger, int, Exception?> _errorProcessingMessage;
private static readonly Action<ILogger, int, int, string, Exception?> _processorMatched;
private static readonly Action<ILogger, int, int, Exception?> _receivedMessageNotRecognized;
private static readonly Action<ILogger, int, string?, Exception?> _failedToDeserializeMessage;
private static readonly Action<ILogger, int, string, Exception?> _userMessageProcessingFailed;
private static readonly Action<ILogger, int, long, long, Exception?> _messageProcessed;
private static readonly Action<ILogger, int, int, Exception?> _closingSubscription;
private static readonly Action<ILogger, int, Exception?> _notUnsubscribingSubscriptionBecauseDuplicateRunning;
private static readonly Action<ILogger, int, Exception?> _alreadyClosing;
private static readonly Action<ILogger, int, Exception?> _closingNoMoreSubscriptions;
private static readonly Action<ILogger, int, int, int, Exception?> _addingNewSubscription;
private static readonly Action<ILogger, int, Exception?> _nothingToResubscribeCloseConnection;
private static readonly Action<ILogger, int, Exception?> _failedAuthenticationDisconnectAndRecoonect;
private static readonly Action<ILogger, int, Exception?> _authenticationSucceeded;
private static readonly Action<ILogger, int, string?, Exception?> _failedRequestRevitalization;
private static readonly Action<ILogger, int, Exception?> _allSubscriptionResubscribed;
private static readonly Action<ILogger, int, int, Exception?> _subscriptionUnsubscribed;
private static readonly Action<ILogger, int, string, Exception?> _sendingPeriodic;
private static readonly Action<ILogger, int, string, string, Exception?> _periodicSendFailed;
private static readonly Action<ILogger, int, int, string, Exception?> _sendingData;
private static readonly Action<ILogger, int, string, string, Exception?> _receivedMessageNotMatchedToAnyListener;
static SocketConnectionLoggingExtension()
{
_activityPaused = LoggerMessage.Define<int, bool>(
LogLevel.Information,
new EventId(2000, "ActivityPaused"),
"[Sckt {SocketId}] paused activity: {Paused}");
_socketStatusChanged = LoggerMessage.Define<int, Sockets.SocketConnection.SocketStatus, Sockets.SocketConnection.SocketStatus>(
LogLevel.Debug,
new EventId(2001, "SocketStatusChanged"),
"[Sckt {SocketId}] status changed from {OldStatus} to {NewStatus}");
_failedReconnectProcessing = LoggerMessage.Define<int, string?>(
LogLevel.Warning,
new EventId(2002, "FailedReconnectProcessing"),
"[Sckt {SocketId}] failed reconnect processing: {ErrorMessage}, reconnecting again");
_unkownExceptionWhileProcessingReconnection = LoggerMessage.Define<int>(
LogLevel.Warning,
new EventId(2003, "UnkownExceptionWhileProcessingReconnection"),
"[Sckt {SocketId}] Unknown exception while processing reconnection, reconnecting again");
_webSocketErrorCodeAndDetails = LoggerMessage.Define<int, WebSocketError, string?>(
LogLevel.Warning,
new EventId(2004, "WebSocketErrorCode"),
"[Sckt {SocketId}] error: Websocket error code {WebSocketErrorCdoe}, details: {Details}");
_webSocketError = LoggerMessage.Define<int, string?>(
LogLevel.Warning,
new EventId(2005, "WebSocketError"),
"[Sckt {SocketId}] error: {ErrorMessage}");
_messageSentNotPending = LoggerMessage.Define<int, int>(
LogLevel.Debug,
new EventId(2006, "MessageSentNotPending"),
"[Sckt {SocketId}] msg {RequestId} - message sent, but not pending");
_receivedData = LoggerMessage.Define<int, string>(
LogLevel.Trace,
new EventId(2007, "ReceivedData"),
"[Sckt {SocketId}] received {OriginalData}");
_failedToEvaluateMessage = LoggerMessage.Define<int, string>(
LogLevel.Warning,
new EventId(2008, "FailedToEvaluateMessage"),
"[Sckt {SocketId}] failed to evaluate message. {OriginalData}");
_errorProcessingMessage = LoggerMessage.Define<int>(
LogLevel.Error,
new EventId(2009, "ErrorProcessingMessage"),
"[Sckt {SocketId}] error processing message");
_processorMatched = LoggerMessage.Define<int, int, string>(
LogLevel.Trace,
new EventId(2010, "ProcessorMatched"),
"[Sckt {SocketId}] {Count} processor(s) matched to message with listener identifier {ListenerId}");
_receivedMessageNotRecognized = LoggerMessage.Define<int, int>(
LogLevel.Warning,
new EventId(2011, "ReceivedMessageNotRecognized"),
"[Sckt {SocketId}] received message not recognized by handler {ProcessorId}");
_failedToDeserializeMessage = LoggerMessage.Define<int, string?>(
LogLevel.Warning,
new EventId(2012, "FailedToDeserializeMessage"),
"[Sckt {SocketId}] deserialization failed: {ErrorMessage}");
_userMessageProcessingFailed = LoggerMessage.Define<int, string>(
LogLevel.Warning,
new EventId(2013, "UserMessageProcessingFailed"),
"[Sckt {SocketId}] user message processing failed: {ErrorMessage}");
_messageProcessed = LoggerMessage.Define<int, long, long>(
LogLevel.Trace,
new EventId(2014, "MessageProcessed"),
"[Sckt {SocketId}] message processed in {ProcessingTime}ms, {ParsingTime}ms parsing");
_closingSubscription = LoggerMessage.Define<int, int>(
LogLevel.Debug,
new EventId(2015, "ClosingSubscription"),
"[Sckt {SocketId}] closing subscription {SubscriptionId}");
_notUnsubscribingSubscriptionBecauseDuplicateRunning = LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(2016, "NotUnsubscribingSubscription"),
"[Sckt {SocketId}] not unsubscribing subscription as there is still a duplicate subscription running");
_alreadyClosing = LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(2017, "AlreadyClosing"),
"[Sckt {SocketId}] already closing");
_closingNoMoreSubscriptions = LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(2018, "ClosingNoMoreSubscriptions"),
"[Sckt {SocketId}] closing as there are no more subscriptions");
_addingNewSubscription = LoggerMessage.Define<int, int, int>(
LogLevel.Debug,
new EventId(2019, "AddingNewSubscription"),
"[Sckt {SocketId}] adding new subscription with id {SubscriptionId}, total subscriptions on connection: {UserSubscriptionCount}");
_nothingToResubscribeCloseConnection = LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(2020, "NothingToResubscribe"),
"[Sckt {SocketId}] nothing to resubscribe, closing connection");
_failedAuthenticationDisconnectAndRecoonect = LoggerMessage.Define<int>(
LogLevel.Warning,
new EventId(2021, "FailedAuthentication"),
"[Sckt {SocketId}] authentication failed on reconnected socket. Disconnecting and reconnecting");
_authenticationSucceeded = LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(2022, "AuthenticationSucceeded"),
"[Sckt {SocketId}] authentication succeeded on reconnected socket");
_failedRequestRevitalization = LoggerMessage.Define<int, string?>(
LogLevel.Warning,
new EventId(2023, "FailedRequestRevitalization"),
"[Sckt {SocketId}] failed request revitalization: {ErrorMessage}");
_allSubscriptionResubscribed = LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(2024, "AllSubscriptionResubscribed"),
"[Sckt {SocketId}] all subscription successfully resubscribed on reconnected socket");
_subscriptionUnsubscribed = LoggerMessage.Define<int, int>(
LogLevel.Information,
new EventId(2025, "SubscriptionUnsubscribed"),
"[Sckt {SocketId}] subscription {SubscriptionId} unsubscribed");
_sendingPeriodic = LoggerMessage.Define<int, string>(
LogLevel.Trace,
new EventId(2026, "SendingPeriodic"),
"[Sckt {SocketId}] sending periodic {Identifier}");
_periodicSendFailed = LoggerMessage.Define<int, string, string>(
LogLevel.Warning,
new EventId(2027, "PeriodicSendFailed"),
"[Sckt {SocketId}] Periodic send {Identifier} failed: {ErrorMessage}");
_sendingData = LoggerMessage.Define<int, int, string>(
LogLevel.Trace,
new EventId(2028, "SendingData"),
"[Sckt {SocketId}] msg {RequestId} - sending messsage: {Data}");
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string>(
LogLevel.Warning,
new EventId(2029, "ReceivedMessageNotMatchedToAnyListener"),
"[Sckt {SocketId}] received message not matched to any listener. ListenId: {ListenId}, current listeners: {ListenIds}");
}
public static void ActivityPaused(this ILogger logger, int socketId, bool paused)
{
_activityPaused(logger, socketId, paused, null);
}
public static void SocketStatusChanged(this ILogger logger, int socketId, Sockets.SocketConnection.SocketStatus oldStatus, Sockets.SocketConnection.SocketStatus newStatus)
{
_socketStatusChanged(logger, socketId, oldStatus, newStatus, null);
}
public static void FailedReconnectProcessing(this ILogger logger, int socketId, string? error)
{
_failedReconnectProcessing(logger, socketId, error, null);
}
public static void UnkownExceptionWhileProcessingReconnection(this ILogger logger, int socketId, Exception e)
{
_unkownExceptionWhileProcessingReconnection(logger, socketId, e);
}
public static void WebSocketErrorCodeAndDetails(this ILogger logger, int socketId, WebSocketError error, string? details, Exception e)
{
_webSocketErrorCodeAndDetails(logger, socketId, error, details, e);
}
public static void WebSocketError(this ILogger logger, int socketId, string? errorMessage, Exception e)
{
_webSocketError(logger, socketId, errorMessage, e);
}
public static void MessageSentNotPending(this ILogger logger, int socketId, int requestId)
{
_messageSentNotPending(logger, socketId, requestId, null);
}
public static void ReceivedData(this ILogger logger, int socketId, string originalData)
{
_receivedData(logger, socketId, originalData, null);
}
public static void FailedToEvaluateMessage(this ILogger logger, int socketId, string originalData)
{
_failedToEvaluateMessage(logger, socketId, originalData, null);
}
public static void ErrorProcessingMessage(this ILogger logger, int socketId, Exception e)
{
_errorProcessingMessage(logger, socketId, e);
}
public static void ProcessorMatched(this ILogger logger, int socketId, int count, string listenerId)
{
_processorMatched(logger, socketId, count, listenerId, null);
}
public static void ReceivedMessageNotRecognized(this ILogger logger, int socketId, int id)
{
_receivedMessageNotRecognized(logger, socketId, id, null);
}
public static void FailedToDeserializeMessage(this ILogger logger, int socketId, string? errorMessage)
{
_failedToDeserializeMessage(logger, socketId, errorMessage, null);
}
public static void UserMessageProcessingFailed(this ILogger logger, int socketId, string errorMessage, Exception e)
{
_userMessageProcessingFailed(logger, socketId, errorMessage, e);
}
public static void MessageProcessed(this ILogger logger, int socketId, long processingTime, long parsingTime)
{
_messageProcessed(logger, socketId, processingTime, parsingTime, null);
}
public static void ClosingSubscription(this ILogger logger, int socketId, int subscriptionId)
{
_closingSubscription(logger, socketId, subscriptionId, null);
}
public static void NotUnsubscribingSubscriptionBecauseDuplicateRunning(this ILogger logger, int socketId)
{
_notUnsubscribingSubscriptionBecauseDuplicateRunning(logger, socketId, null);
}
public static void AlreadyClosing(this ILogger logger, int socketId)
{
_alreadyClosing(logger, socketId, null);
}
public static void ClosingNoMoreSubscriptions(this ILogger logger, int socketId)
{
_closingNoMoreSubscriptions(logger, socketId, null);
}
public static void AddingNewSubscription(this ILogger logger, int socketId, int subscriptionId, int userSubscriptionCount)
{
_addingNewSubscription(logger, socketId, subscriptionId, userSubscriptionCount, null);
}
public static void NothingToResubscribeCloseConnection(this ILogger logger, int socketId)
{
_nothingToResubscribeCloseConnection(logger, socketId, null);
}
public static void FailedAuthenticationDisconnectAndRecoonect(this ILogger logger, int socketId)
{
_failedAuthenticationDisconnectAndRecoonect(logger, socketId, null);
}
public static void AuthenticationSucceeded(this ILogger logger, int socketId)
{
_authenticationSucceeded(logger, socketId, null);
}
public static void FailedRequestRevitalization(this ILogger logger, int socketId, string? errorMessage)
{
_failedRequestRevitalization(logger, socketId, errorMessage, null);
}
public static void AllSubscriptionResubscribed(this ILogger logger, int socketId)
{
_allSubscriptionResubscribed(logger, socketId, null);
}
public static void SubscriptionUnsubscribed(this ILogger logger, int socketId, int subscriptionId)
{
_subscriptionUnsubscribed(logger, socketId, subscriptionId, null);
}
public static void SendingPeriodic(this ILogger logger, int socketId, string identifier)
{
_sendingPeriodic(logger, socketId, identifier, null);
}
public static void PeriodicSendFailed(this ILogger logger, int socketId, string identifier, string errorMessage, Exception e)
{
_periodicSendFailed(logger, socketId, identifier, errorMessage, e);
}
public static void SendingData(this ILogger logger, int socketId, int requestId, string data)
{
_sendingData(logger, socketId, requestId, data, null);
}
public static void ReceivedMessageNotMatchedToAnyListener(this ILogger logger, int socketId, string listenId, string listenIds)
{
_receivedMessageNotMatchedToAnyListener(logger, socketId, listenId, listenIds, null);
}
}
}
@@ -1,236 +0,0 @@
using System;
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Logging.Extensions
{
internal static class SymbolOrderBookLoggingExtensions
{
private static readonly Action<ILogger, string, string, OrderBookStatus, OrderBookStatus, Exception?> _orderBookStatusChanged;
private static readonly Action<ILogger, string, string, Exception?> _orderBookStarting;
private static readonly Action<ILogger, string, string, Exception?> _orderBookStoppedStarting;
private static readonly Action<ILogger, string, string, Exception?> _orderBookStopping;
private static readonly Action<ILogger, string, string, Exception?> _orderBookStopped;
private static readonly Action<ILogger, string, string, Exception?> _orderBookConnectionLost;
private static readonly Action<ILogger, string, string, Exception?> _orderBookDisconnected;
private static readonly Action<ILogger, string, string, int, Exception?> _orderBookProcessingBufferedUpdates;
private static readonly Action<ILogger, string, string, long, long, Exception?> _orderBookUpdateSkipped;
private static readonly Action<ILogger, string, string, Exception?> _orderBookOutOfSyncChecksum;
private static readonly Action<ILogger, string, string, Exception?> _orderBookResyncFailed;
private static readonly Action<ILogger, string, string, Exception?> _orderBookResyncing;
private static readonly Action<ILogger, string, string, Exception?> _orderBookResynced;
private static readonly Action<ILogger, string, string, Exception?> _orderBookMessageSkippedBecauseOfResubscribing;
private static readonly Action<ILogger, string, string, long, long, long, Exception?> _orderBookDataSet;
private static readonly Action<ILogger, string, string, long, long, long, long, Exception?> _orderBookUpdateBuffered;
private static readonly Action<ILogger, string, string, decimal, decimal, Exception?> _orderBookOutOfSyncDetected;
private static readonly Action<ILogger, string, string, Exception?> _orderBookReconnectingSocket;
private static readonly Action<ILogger, string, string, long, long, Exception?> _orderBookSkippedMessage;
private static readonly Action<ILogger, string, string, long, long, Exception?> _orderBookProcessedMessage;
private static readonly Action<ILogger, string, string, long, long, Exception?> _orderBookOutOfSync;
static SymbolOrderBookLoggingExtensions()
{
_orderBookStatusChanged = LoggerMessage.Define<string, string, OrderBookStatus, OrderBookStatus>(
LogLevel.Information,
new EventId(5000, "OrderBookStatusChanged"),
"{Api} order book {Symbol} status changed: {PreviousStatus} => {NewStatus}");
_orderBookStarting = LoggerMessage.Define<string, string>(
LogLevel.Debug,
new EventId(5001, "OrderBookStarting"),
"{Api} order book {Symbol} starting");
_orderBookStoppedStarting = LoggerMessage.Define<string, string>(
LogLevel.Debug,
new EventId(5002, "OrderBookStoppedStarting"),
"{Api} order book {Symbol} stopped while starting");
_orderBookConnectionLost = LoggerMessage.Define<string, string>(
LogLevel.Warning,
new EventId(5003, "OrderBookConnectionLost"),
"{Api} order book {Symbol} connection lost");
_orderBookDisconnected = LoggerMessage.Define<string, string>(
LogLevel.Warning,
new EventId(5004, "OrderBookDisconnected"),
"{Api} order book {Symbol} disconnected");
_orderBookStopping = LoggerMessage.Define<string, string>(
LogLevel.Debug,
new EventId(5005, "OrderBookStopping"),
"{Api} order book {Symbol} stopping");
_orderBookStopped = LoggerMessage.Define<string, string>(
LogLevel.Trace,
new EventId(5006, "OrderBookStopped"),
"{Api} order book {Symbol} stopped");
_orderBookProcessingBufferedUpdates = LoggerMessage.Define<string, string, int>(
LogLevel.Debug,
new EventId(5007, "OrderBookProcessingBufferedUpdates"),
"{Api} order book {Symbol} Processing {NumberBufferedUpdated} buffered updates");
_orderBookUpdateSkipped = LoggerMessage.Define<string, string, long, long>(
LogLevel.Debug,
new EventId(5008, "OrderBookUpdateSkipped"),
"{Api} order book {Symbol} update skipped #{SequenceNumber}, currently at #{LastSequenceNumber}");
_orderBookOutOfSync = LoggerMessage.Define<string, string, long, long>(
LogLevel.Warning,
new EventId(5009, "OrderBookOutOfSync"),
"{Api} order book {Symbol} out of sync (expected {ExpectedSequenceNumber}, was {SequenceNumber}), reconnecting");
_orderBookResynced = LoggerMessage.Define<string, string>(
LogLevel.Information,
new EventId(5010, "OrderBookResynced"),
"{Api} order book {Symbol} successfully resynchronized");
_orderBookMessageSkippedBecauseOfResubscribing = LoggerMessage.Define<string, string>(
LogLevel.Trace,
new EventId(5011, "OrderBookMessageSkippedResubscribing"),
"{Api} order book {Symbol} Skipping message because of resubscribing");
_orderBookDataSet = LoggerMessage.Define<string, string, long, long, long>(
LogLevel.Debug,
new EventId(5012, "OrderBookDataSet"),
"{Api} order book {Symbol} data set: {BidCount} bids, {AskCount} asks. #{EndUpdateId}");
_orderBookUpdateBuffered = LoggerMessage.Define<string, string, long, long, long, long>(
LogLevel.Trace,
new EventId(5013, "OrderBookUpdateBuffered"),
"{Api} order book {Symbol} update buffered #{StartUpdateId}-#{EndUpdateId} [{AsksCount} asks, {BidsCount} bids]");
_orderBookOutOfSyncDetected = LoggerMessage.Define<string, string, decimal, decimal>(
LogLevel.Warning,
new EventId(5014, "OrderBookOutOfSyncDetected"),
"{Api} order book {Symbol} detected out of sync order book. First ask: {FirstAsk}, first bid: {FirstBid}. Resyncing");
_orderBookReconnectingSocket = LoggerMessage.Define<string, string>(
LogLevel.Warning,
new EventId(5015, "OrderBookReconnectingSocket"),
"{Api} order book {Symbol} out of sync. Reconnecting socket");
_orderBookResyncing = LoggerMessage.Define<string, string>(
LogLevel.Warning,
new EventId(5016, "OrderBookResyncing"),
"{Api} order book {Symbol} out of sync. Resyncing");
_orderBookResyncFailed = LoggerMessage.Define<string, string>(
LogLevel.Warning,
new EventId(5017, "OrderBookResyncFailed"),
"{Api} order book {Symbol} resync failed, reconnecting socket");
_orderBookSkippedMessage = LoggerMessage.Define<string, string, long, long>(
LogLevel.Trace,
new EventId(5018, "OrderBookSkippedMessage"),
"{Api} order book {Symbol} update skipped #{FirstUpdateId}-{LastUpdateId}");
_orderBookProcessedMessage = LoggerMessage.Define<string, string, long, long>(
LogLevel.Trace,
new EventId(5019, "OrderBookProcessedMessage"),
"{Api} order book {Symbol} update processed #{FirstUpdateId}-{LastUpdateId}");
_orderBookOutOfSyncChecksum = LoggerMessage.Define<string, string>(
LogLevel.Warning,
new EventId(5020, "OrderBookOutOfSyncChecksum"),
"{Api} order book {Symbol} out of sync. Checksum mismatch, resyncing");
}
public static void OrderBookStatusChanged(this ILogger logger, string api, string symbol, OrderBookStatus previousStatus, OrderBookStatus newStatus)
{
_orderBookStatusChanged(logger, api, symbol, previousStatus, newStatus, null);
}
public static void OrderBookStarting(this ILogger logger, string api, string symbol)
{
_orderBookStarting(logger, api, symbol, null);
}
public static void OrderBookStoppedStarting(this ILogger logger, string api, string symbol)
{
_orderBookStoppedStarting(logger, api, symbol, null);
}
public static void OrderBookConnectionLost(this ILogger logger, string api, string symbol)
{
_orderBookConnectionLost(logger, api, symbol, null);
}
public static void OrderBookDisconnected(this ILogger logger, string api, string symbol)
{
_orderBookDisconnected(logger, api, symbol, null);
}
public static void OrderBookStopping(this ILogger logger, string api, string symbol)
{
_orderBookStopping(logger, api, symbol, null);
}
public static void OrderBookStopped(this ILogger logger, string api, string symbol)
{
_orderBookStopped(logger, api, symbol, null);
}
public static void OrderBookProcessingBufferedUpdates(this ILogger logger, string api, string symbol, int numberBufferedUpdated)
{
_orderBookProcessingBufferedUpdates(logger, api, symbol, numberBufferedUpdated, null);
}
public static void OrderBookUpdateSkipped(this ILogger logger, string api, string symbol, long sequence, long lastSequenceNumber)
{
_orderBookUpdateSkipped(logger, api, symbol, sequence, lastSequenceNumber, null);
}
public static void OrderBookOutOfSync(this ILogger logger, string api, string symbol, long expectedSequenceNumber, long sequenceNumber)
{
_orderBookOutOfSync(logger, api, symbol, expectedSequenceNumber, sequenceNumber, null);
}
public static void OrderBookResynced(this ILogger logger, string api, string symbol)
{
_orderBookResynced(logger, api, symbol, null);
}
public static void OrderBookMessageSkippedResubscribing(this ILogger logger, string api, string symbol)
{
_orderBookMessageSkippedBecauseOfResubscribing(logger, api, symbol, null);
}
public static void OrderBookDataSet(this ILogger logger, string api, string symbol, long bidCount, long askCount, long endUpdateId)
{
_orderBookDataSet(logger, api, symbol, bidCount, askCount, endUpdateId, null);
}
public static void OrderBookUpdateBuffered(this ILogger logger, string api, string symbol, long startUpdateId, long endUpdateId, long asksCount, long bidsCount)
{
_orderBookUpdateBuffered(logger, api, symbol, startUpdateId, endUpdateId, asksCount, bidsCount, null);
}
public static void OrderBookOutOfSyncDetected(this ILogger logger, string api, string symbol, decimal firstAsk, decimal firstBid)
{
_orderBookOutOfSyncDetected(logger, api, symbol, firstAsk, firstBid, null);
}
public static void OrderBookReconnectingSocket(this ILogger logger, string api, string symbol)
{
_orderBookReconnectingSocket(logger, api, symbol, null);
}
public static void OrderBookResyncing(this ILogger logger, string api, string symbol)
{
_orderBookResyncing(logger, api, symbol, null);
}
public static void OrderBookResyncFailed(this ILogger logger, string api, string symbol)
{
_orderBookResyncFailed(logger, api, symbol, null);
}
public static void OrderBookSkippedMessage(this ILogger logger, string api, string symbol, long firstUpdateId, long lastUpdateId)
{
_orderBookSkippedMessage(logger, api, symbol, firstUpdateId, lastUpdateId, null);
}
public static void OrderBookProcessedMessage(this ILogger logger, string api, string symbol, long firstUpdateId, long lastUpdateId)
{
_orderBookProcessedMessage(logger, api, symbol, firstUpdateId, lastUpdateId, null);
}
public static void OrderBookOutOfSyncChecksum(this ILogger logger, string api, string symbol)
{
_orderBookOutOfSyncChecksum(logger, api, symbol, null);
}
}
}
+3 -10
View File
@@ -99,22 +99,15 @@
/// Define how array parameters should be send
/// </summary>
public enum ArrayParametersSerialization
#pragma warning disable CS1570 // XML comment has badly formed XML
{
/// <summary>
/// Send as key=value1&key=value2
/// Send multiple key=value for each entry
/// </summary>
MultipleValues,
/// <summary>
/// Send as key[]=value1&key[]=value2
/// Create an []=value array
/// </summary>
Array,
/// <summary>
/// Send as key=[value1, value2]
/// </summary>
JsonArray
#pragma warning restore CS1570 // XML comment has badly formed XML
Array
}
/// <summary>
@@ -1,6 +1,5 @@
using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.Converters.SystemTextJson;
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -159,17 +158,6 @@ namespace CryptoExchange.Net.Objects
Add(key, EnumConverter.GetString(value)!);
}
/// <summary>
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddEnumAsInt<T>(string key, T value)
{
var stringVal = EnumConverter.GetString(value);
Add(key, EnumConverter.GetString(int.Parse(stringVal))!);
}
/// <summary>
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />. Not added if value is null
/// </summary>
@@ -180,19 +168,5 @@ namespace CryptoExchange.Net.Objects
if (value != null)
Add(key, EnumConverter.GetString(value));
}
/// <summary>
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddOptionalEnumAsInt<T>(string key, T? value)
{
if (value != null)
{
var stringVal = EnumConverter.GetString(value);
Add(key, int.Parse(stringVal));
}
}
}
}
+5 -5
View File
@@ -31,8 +31,8 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public class TraceLogger : ILogger
{
private readonly string? _categoryName;
private readonly LogLevel _logLevel;
private string? _categoryName;
private LogLevel _logLevel;
/// <summary>
/// ctor
@@ -46,14 +46,14 @@ namespace CryptoExchange.Net.Objects
}
/// <inheritdoc />
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null!;
public IDisposable BeginScope<TState>(TState state) => null!;
/// <inheritdoc />
public bool IsEnabled(LogLevel logLevel) => (int)logLevel >= (int)_logLevel;
public bool IsEnabled(LogLevel logLevel) => (int)logLevel < (int)_logLevel;
/// <inheritdoc />
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel))
if ((int)logLevel < (int)_logLevel)
return;
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {(_categoryName == null ? "" : $"{_categoryName} | ")}{formatter(state, exception)}";
+26 -38
View File
@@ -7,12 +7,10 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace CryptoExchange.Net.OrderBook
{
@@ -90,10 +88,7 @@ namespace CryptoExchange.Net.OrderBook
protected int? Levels { get; set; } = null;
/// <inheritdoc/>
public string Exchange { get; }
/// <inheritdoc/>
public string Api { get; }
public string Id { get; }
/// <inheritdoc/>
public OrderBookStatus Status
@@ -106,7 +101,7 @@ namespace CryptoExchange.Net.OrderBook
var old = _status;
_status = value;
_logger.OrderBookStatusChanged(Api, Symbol, old, value);
_logger.Log(LogLevel.Information, $"{Id} order book {Symbol} status changed: {old} => {value}");
OnStatusChange?.Invoke(old, _status);
}
}
@@ -197,17 +192,14 @@ namespace CryptoExchange.Net.OrderBook
/// ctor
/// </summary>
/// <param name="logger">Logger to use. If not provided will create a TraceLogger</param>
/// <param name="exchange">The exchange of the order book</param>
/// <param name="api">The API the book is for, for example Spot</param>
/// <param name="id">The id of the order book. Should be set to {Exchange}[{type}], for example: Kucoin[Spot]</param>
/// <param name="symbol">The symbol the order book is for</param>
protected SymbolOrderBook(ILoggerFactory? logger, string exchange, string api, string symbol)
protected SymbolOrderBook(ILogger? logger, string id, string symbol)
{
if (symbol == null)
throw new ArgumentNullException(nameof(symbol));
Exchange = exchange;
Api = api;
Id = id;
_processBuffer = new List<ProcessBufferRangeSequenceEntry>();
_processQueue = new ConcurrentQueue<object>();
_queueEvent = new AsyncResetEvent(false, true);
@@ -218,7 +210,7 @@ namespace CryptoExchange.Net.OrderBook
_asks = new SortedList<decimal, ISymbolOrderBookEntry>();
_bids = new SortedList<decimal, ISymbolOrderBookEntry>(new DescComparer<decimal>());
_logger = logger?.CreateLogger(Exchange) ?? NullLoggerFactory.Instance.CreateLogger(Exchange);
_logger = logger ?? new TraceLogger();
}
/// <summary>
@@ -240,7 +232,7 @@ namespace CryptoExchange.Net.OrderBook
if (Status != OrderBookStatus.Disconnected)
throw new InvalidOperationException($"Can't start book unless state is {OrderBookStatus.Disconnected}. Current state: {Status}");
_logger.OrderBookStarting(Api, Symbol);
_logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} starting");
_cts = new CancellationTokenSource();
ct?.Register(async () =>
{
@@ -265,7 +257,7 @@ namespace CryptoExchange.Net.OrderBook
if (_cts.IsCancellationRequested)
{
_logger.OrderBookStoppedStarting(Api, Symbol);
_logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} stopped while starting");
await startResult.Data.CloseAsync().ConfigureAwait(false);
Status = OrderBookStatus.Disconnected;
return new CallResult<bool>(new CancellationRequestedError());
@@ -280,17 +272,16 @@ namespace CryptoExchange.Net.OrderBook
return new CallResult<bool>(true);
}
private void HandleConnectionLost()
{
_logger.OrderBookConnectionLost(Api, Symbol);
if (Status != OrderBookStatus.Disposed) {
private void HandleConnectionLost() {
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} connection lost");
if (Status != OrderBookStatus.Disposed) {
Status = OrderBookStatus.Reconnecting;
Reset();
}
}
private void HandleConnectionClosed() {
_logger.OrderBookDisconnected(Api, Symbol);
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} disconnected");
Status = OrderBookStatus.Disconnected;
_ = StopAsync();
}
@@ -302,7 +293,7 @@ namespace CryptoExchange.Net.OrderBook
/// <inheritdoc/>
public async Task StopAsync()
{
_logger.OrderBookStopping(Api, Symbol);
_logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} stopping");
Status = OrderBookStatus.Disconnected;
_cts?.Cancel();
_queueEvent.Set();
@@ -315,8 +306,7 @@ namespace CryptoExchange.Net.OrderBook
_subscription.ConnectionClosed -= HandleConnectionClosed;
_subscription.ConnectionRestored -= HandleConnectionRestored;
}
_logger.OrderBookStopped(Api, Symbol);
_logger.Log(LogLevel.Trace, $"{Id} order book {Symbol} stopped");
}
/// <inheritdoc/>
@@ -474,7 +464,7 @@ namespace CryptoExchange.Net.OrderBook
{
var pbList = _processBuffer.ToList();
if (pbList.Count > 0)
_logger.OrderBookProcessingBufferedUpdates(Api, Symbol, pbList.Count);
_logger.Log(LogLevel.Debug, $"{Id} Processing {pbList.Count} buffered updates");
foreach (var bufferEntry in pbList)
{
@@ -493,14 +483,14 @@ namespace CryptoExchange.Net.OrderBook
{
if (sequence <= LastSequenceNumber)
{
_logger.OrderBookSkippedMessage(Api, Symbol, sequence, LastSequenceNumber);
_logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} update skipped #{sequence}, currently at #{LastSequenceNumber}");
return false;
}
if (_sequencesAreConsecutive && sequence > LastSequenceNumber + 1)
{
// Out of sync
_logger.OrderBookOutOfSync(Api, Symbol, LastSequenceNumber + 1, sequence);
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} out of sync (expected { LastSequenceNumber + 1}, was {sequence}), reconnecting");
_stopProcessing = true;
Resubscribe();
return false;
@@ -654,7 +644,7 @@ namespace CryptoExchange.Net.OrderBook
success = resyncResult;
}
_logger.OrderBookResynced(Api, Symbol);
_logger.Log(LogLevel.Information, $"{Id} order book {Symbol} successfully resynchronized");
Status = OrderBookStatus.Synced;
}
@@ -671,7 +661,7 @@ namespace CryptoExchange.Net.OrderBook
if (_stopProcessing)
{
_logger.OrderBookMessageSkippedResubscribing(Api, Symbol);
_logger.Log(LogLevel.Trace, $"{Id} Skipping message because of resubscribing");
continue;
}
@@ -703,7 +693,7 @@ namespace CryptoExchange.Net.OrderBook
BidCount = _bids.Count;
UpdateTime = DateTime.UtcNow;
_logger.OrderBookDataSet(Api, Symbol, BidCount, AskCount, item.EndUpdateId);
_logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks. #{item.EndUpdateId}");
CheckProcessBuffer();
OnOrderBookUpdate?.Invoke((item.Bids, item.Asks));
OnBestOffersChanged?.Invoke((BestBid, BestAsk));
@@ -723,8 +713,7 @@ namespace CryptoExchange.Net.OrderBook
FirstUpdateId = item.StartUpdateId,
LastUpdateId = item.EndUpdateId,
});
_logger.OrderBookUpdateBuffered(Api, Symbol, item.StartUpdateId, item.EndUpdateId, item.Asks.Count(), item.Bids.Count());
_logger.Log(LogLevel.Trace, $"{Id} order book {Symbol} update buffered #{item.StartUpdateId}-#{item.EndUpdateId} [{item.Asks.Count()} asks, {item.Bids.Count()} bids]");
}
else
{
@@ -737,7 +726,7 @@ namespace CryptoExchange.Net.OrderBook
if (_asks.First().Key < _bids.First().Key)
{
_logger.OrderBookOutOfSyncDetected(Api, Symbol, _asks.First().Key, _bids.First().Key);
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} detected out of sync order book. First ask: {_asks.First().Key}, first bid: {_bids.First().Key}. Resyncing");
_stopProcessing = true;
Resubscribe();
return;
@@ -771,7 +760,7 @@ namespace CryptoExchange.Net.OrderBook
if (!checksumResult)
{
_logger.OrderBookOutOfSyncChecksum(Api, Symbol);
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} out of sync. Resyncing");
_stopProcessing = true;
Resubscribe();
}
@@ -795,7 +784,7 @@ namespace CryptoExchange.Net.OrderBook
if (!await _subscription!.ResubscribeAsync().ConfigureAwait(false))
{
// Resubscribing failed, reconnect the socket
_logger.OrderBookResyncFailed(Api, Symbol);
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} resync failed, reconnecting socket");
Status = OrderBookStatus.Reconnecting;
_ = _subscription!.ReconnectAsync();
}
@@ -810,7 +799,7 @@ namespace CryptoExchange.Net.OrderBook
{
if (lastUpdateId <= LastSequenceNumber)
{
_logger.OrderBookUpdateSkipped(Api, Symbol, firstUpdateId, lastUpdateId);
_logger.Log(LogLevel.Trace, $"{Id} order book {Symbol} update skipped #{firstUpdateId}-{lastUpdateId}");
return;
}
@@ -836,8 +825,7 @@ namespace CryptoExchange.Net.OrderBook
}
LastSequenceNumber = lastUpdateId;
_logger.OrderBookProcessedMessage(Api, Symbol, firstUpdateId, lastUpdateId);
_logger.Log(LogLevel.Trace, $"{Id} order book {Symbol} update processed #{firstUpdateId}-{lastUpdateId}");
}
}
@@ -1,5 +1,4 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging;
@@ -46,9 +45,6 @@ namespace CryptoExchange.Net.Sockets
private ProcessState _processState;
private DateTime _lastReconnectTime;
private const int _receiveBufferSize = 1048576;
private const int _sendBufferSize = 4096;
/// <summary>
/// Received messages, the size and the timstamp
/// </summary>
@@ -105,7 +101,7 @@ namespace CryptoExchange.Net.Sockets
public event Func<Task>? OnClose;
/// <inheritdoc />
public event Action<WebSocketMessageType, ReadOnlyMemory<byte>>? OnStreamMessage;
public event Func<WebSocketMessageType, Stream, Task>? OnStreamMessage;
/// <inheritdoc />
public event Func<int, Task>? OnRequestSent;
@@ -172,10 +168,7 @@ namespace CryptoExchange.Net.Sockets
foreach (var header in Parameters.Headers)
socket.Options.SetRequestHeader(header.Key, header.Value);
socket.Options.KeepAliveInterval = Parameters.KeepAliveInterval ?? TimeSpan.Zero;
if (System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework"))
socket.Options.SetBuffer(65536, 65536); // Setting it to anything bigger than 65536 throws an exception in .net framework
else
socket.Options.SetBuffer(_receiveBufferSize, _sendBufferSize);
socket.Options.SetBuffer(65536, 65536); // Setting it to anything bigger than 65536 throws an exception in .net framework
if (Parameters.Proxy != null)
SetProxy(socket, Parameters.Proxy);
}
@@ -190,24 +183,19 @@ namespace CryptoExchange.Net.Sockets
private async Task<bool> ConnectInternalAsync()
{
_logger.SocketConnecting(Id);
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] connecting");
try
{
using CancellationTokenSource tcs = new(TimeSpan.FromSeconds(10));
using var linked = CancellationTokenSource.CreateLinkedTokenSource(tcs.Token, _ctsSource.Token);
await _socket.ConnectAsync(Uri, linked.Token).ConfigureAwait(false);
await _socket.ConnectAsync(Uri, tcs.Token).ConfigureAwait(false);
}
catch (Exception e)
{
if (!_ctsSource.IsCancellationRequested)
{
// if _ctsSource was canceled this was already logged
_logger.SocketConnectionFailed(Id, e.Message, e);
}
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] connection failed: " + e.ToLogString());
return false;
}
_logger.SocketConnected(Id, Uri);
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] connected to {Uri}");
return true;
}
@@ -216,13 +204,13 @@ namespace CryptoExchange.Net.Sockets
{
while (!_stopRequested)
{
_logger.SocketStartingProcessing(Id);
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] starting processing tasks");
_processState = ProcessState.Processing;
var sendTask = SendLoopAsync();
var receiveTask = ReceiveLoopAsync();
var timeoutTask = Parameters.Timeout != null && Parameters.Timeout > TimeSpan.FromSeconds(0) ? CheckTimeoutAsync() : Task.CompletedTask;
await Task.WhenAll(sendTask, receiveTask, timeoutTask).ConfigureAwait(false);
_logger.SocketFinishedProcessing(Id);
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] processing tasks finished");
_processState = ProcessState.WaitingForClose;
while (_closeTask == null)
@@ -250,14 +238,14 @@ namespace CryptoExchange.Net.Sockets
while (!_stopRequested)
{
_logger.SocketAttemptReconnect(Id);
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] attempting to reconnect");
var task = GetReconnectionUrl?.Invoke();
if (task != null)
{
var reconnectUri = await task.ConfigureAwait(false);
if (reconnectUri != null && Parameters.Uri != reconnectUri)
{
_logger.SocketSetReconnectUri(Id, reconnectUri);
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] reconnect URI set to {reconnectUri}");
Parameters.Uri = reconnectUri;
}
}
@@ -290,7 +278,7 @@ namespace CryptoExchange.Net.Sockets
return;
var bytes = Parameters.Encoding.GetBytes(data);
_logger.SocketAddingBytesToSendBuffer(Id, id, bytes);
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] msg {id} - Adding {bytes.Length} bytes to send buffer");
_sendBuffer.Enqueue(new SendItem { Id = id, Weight = weight, Bytes = bytes });
_sendEvent.Set();
}
@@ -301,7 +289,7 @@ namespace CryptoExchange.Net.Sockets
if (_processState != ProcessState.Processing && IsOpen)
return;
_logger.SocketReconnectRequested(Id);
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] reconnect requested");
_closeTask = CloseInternalAsync();
await _closeTask.ConfigureAwait(false);
}
@@ -316,18 +304,18 @@ namespace CryptoExchange.Net.Sockets
{
if (_closeTask?.IsCompleted == false)
{
_logger.SocketCloseAsyncWaitingForExistingCloseTask(Id);
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] CloseAsync() waiting for existing close task");
await _closeTask.ConfigureAwait(false);
return;
}
if (!IsOpen)
{
_logger.SocketCloseAsyncSocketNotOpen(Id);
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] CloseAsync() socket not open");
return;
}
_logger.SocketClosing(Id);
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] closing");
_closeTask = CloseInternalAsync();
}
finally
@@ -339,7 +327,7 @@ namespace CryptoExchange.Net.Sockets
if(_processTask != null)
await _processTask.ConfigureAwait(false);
await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
_logger.SocketClosed(Id);
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] closed");
}
/// <summary>
@@ -391,14 +379,11 @@ namespace CryptoExchange.Net.Sockets
if (_disposed)
return;
if (_ctsSource?.IsCancellationRequested == false)
_ctsSource.Cancel();
_logger.SocketDisposing(Id);
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] disposing");
_disposed = true;
_socket.Dispose();
_ctsSource?.Dispose();
_logger.SocketDisposed(Id);
_ctsSource.Dispose();
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] disposed");
}
/// <summary>
@@ -430,7 +415,7 @@ namespace CryptoExchange.Net.Sockets
if (limitResult.Success)
{
if (limitResult.Data > 0)
_logger.SocketSendDelayedBecauseOfRateLimit(Id, data.Id, limitResult.Data);
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] msg {data.Id} - send delayed {limitResult.Data}ms because of rate limit");
}
}
}
@@ -439,7 +424,7 @@ namespace CryptoExchange.Net.Sockets
{
await _socket.SendAsync(new ArraySegment<byte>(data.Bytes, 0, data.Bytes.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false);
await (OnRequestSent?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false);
_logger.SocketSentBytes(Id, data.Id, data.Bytes.Length);
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] msg {data.Id} - sent {data.Bytes.Length} bytes");
}
catch (OperationCanceledException)
{
@@ -462,13 +447,13 @@ namespace CryptoExchange.Net.Sockets
// Because this is running in a separate task and not awaited until the socket gets closed
// any exception here will crash the send processing, but do so silently unless the socket get's stopped.
// Make sure we at least let the owner know there was an error
_logger.SocketSendLoopStoppedWithException(Id, e.Message, e);
_logger.Log(LogLevel.Warning, $"[Sckt {Id}] send loop stopped with exception");
await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false);
throw;
}
finally
{
_logger.SocketSendLoopFinished(Id);
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] send loop finished");
}
}
@@ -478,7 +463,7 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns>
private async Task ReceiveLoopAsync()
{
var buffer = new ArraySegment<byte>(new byte[_receiveBufferSize]);
var buffer = new ArraySegment<byte>(new byte[65536]);
var received = 0;
try
{
@@ -487,7 +472,7 @@ namespace CryptoExchange.Net.Sockets
if (_ctsSource.IsCancellationRequested)
break;
MemoryStream? multipartStream = null;
MemoryStream? memoryStream = null;
WebSocketReceiveResult? receiveResult = null;
bool multiPartMessage = false;
while (true)
@@ -515,8 +500,8 @@ namespace CryptoExchange.Net.Sockets
if (receiveResult.MessageType == WebSocketMessageType.Close)
{
// Connection closed unexpectedly
_logger.SocketReceivedCloseMessage(Id, receiveResult.CloseStatus.ToString(), receiveResult.CloseStatusDescription);
// Connection closed unexpectedly
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] received `Close` message");
if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync();
break;
@@ -526,28 +511,24 @@ namespace CryptoExchange.Net.Sockets
{
// We received data, but it is not complete, write it to a memory stream for reassembling
multiPartMessage = true;
_logger.SocketReceivedPartialMessage(Id, receiveResult.Count);
// Write the data to a memory stream to be reassembled later
if (multipartStream == null)
multipartStream = new MemoryStream();
multipartStream.Write(buffer.Array, buffer.Offset, receiveResult.Count);
memoryStream ??= new MemoryStream();
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] received {receiveResult.Count} bytes in partial message");
await memoryStream.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false);
}
else
{
if (!multiPartMessage)
{
// Received a complete message and it's not multi part
_logger.SocketReceivedSingleMessage(Id, receiveResult.Count);
ProcessData(receiveResult.MessageType, new ReadOnlyMemory<byte>(buffer.Array, buffer.Offset, receiveResult.Count));
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] received {receiveResult.Count} bytes in single message");
await ProcessData(receiveResult.MessageType, new MemoryStream(buffer.Array, buffer.Offset, receiveResult.Count)).ConfigureAwait(false);
}
else
{
// Received the end of a multipart message, write to memory stream for reassembling
_logger.SocketReceivedPartialMessage(Id, receiveResult.Count);
multipartStream!.Write(buffer.Array, buffer.Offset, receiveResult.Count);
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] received {receiveResult.Count} bytes in partial message");
await memoryStream!.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false);
}
break;
}
}
@@ -572,13 +553,14 @@ namespace CryptoExchange.Net.Sockets
// When the connection gets interupted we might not have received a full message
if (receiveResult?.EndOfMessage == true)
{
_logger.SocketReassembledMessage(Id, multipartStream!.Length);
// Get the underlying buffer of the memorystream holding the written data and delimit it (GetBuffer return the full array, not only the written part)
ProcessData(receiveResult.MessageType, new ReadOnlyMemory<byte>(multipartStream.GetBuffer(), 0, (int)multipartStream.Length));
// Reassemble complete message from memory stream
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] reassembled message of {memoryStream!.Length} bytes");
await ProcessData(receiveResult.MessageType, memoryStream).ConfigureAwait(false);
memoryStream.Dispose();
}
else
{
_logger.SocketDiscardIncompleteMessage(Id, multipartStream!.Length);
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] discarding incomplete message of {memoryStream!.Length} bytes");
}
}
}
@@ -588,13 +570,13 @@ namespace CryptoExchange.Net.Sockets
// Because this is running in a separate task and not awaited until the socket gets closed
// any exception here will crash the receive processing, but do so silently unless the socket gets stopped.
// Make sure we at least let the owner know there was an error
_logger.SocketReceiveLoopStoppedWithException(Id, e);
_logger.Log(LogLevel.Warning, $"[Sckt {Id}] receive loop stopped with exception");
await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false);
throw;
}
finally
{
_logger.SocketReceiveLoopFinished(Id);
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] receive loop finished");
}
}
@@ -602,12 +584,15 @@ namespace CryptoExchange.Net.Sockets
/// Proccess a stream message
/// </summary>
/// <param name="type"></param>
/// <param name="data"></param>
/// <param name="stream"></param>
/// <returns></returns>
protected void ProcessData(WebSocketMessageType type, ReadOnlyMemory<byte> data)
protected async Task ProcessData(WebSocketMessageType type, Stream stream)
{
LastActionTime = DateTime.UtcNow;
OnStreamMessage?.Invoke(type, data);
stream.Position = 0;
if (OnStreamMessage != null)
await OnStreamMessage.Invoke(type, stream).ConfigureAwait(false);
}
/// <summary>
@@ -616,7 +601,7 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns>
protected async Task CheckTimeoutAsync()
{
_logger.SocketStartingTaskForNoDataReceivedCheck(Id, Parameters.Timeout);
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] starting task checking for no data received for {Parameters.Timeout}");
LastActionTime = DateTime.UtcNow;
try
{
@@ -627,7 +612,7 @@ namespace CryptoExchange.Net.Sockets
if (DateTime.UtcNow - LastActionTime > Parameters.Timeout)
{
_logger.SocketNoDataReceiveTimoutReconnect(Id, Parameters.Timeout);
_logger.Log(LogLevel.Warning, $"[Sckt {Id}] no data received for {Parameters.Timeout}, reconnecting socket");
_ = ReconnectAsync().ConfigureAwait(false);
return;
}
@@ -1,11 +1,8 @@
using CryptoExchange.Net.Converters.MessageParsing;
using CryptoExchange.Net.Objects;
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces
namespace CryptoExchange.Net.Sockets.MessageParsing.Interfaces
{
/// <summary>
/// Message accessor
@@ -17,17 +14,14 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
bool IsJson { get; }
/// <summary>
/// Is the original data available for retrieval
/// </summary>
bool OriginalDataAvailable { get; }
/// <summary>
/// The underlying data object
/// </summary>
object? Underlying { get; }
/// <summary>
/// Clear internal data structure
/// Load a stream message
/// </summary>
void Clear();
/// <param name="stream"></param>
void Load(Stream stream);
/// <summary>
/// Get the type of node
/// </summary>
@@ -59,43 +53,6 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="type"></param>
/// <param name="path"></param>
/// <returns></returns>
CallResult<object> Deserialize(Type type, MessagePath? path = null);
/// <summary>
/// Deserialize the message into this type
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
CallResult<T> Deserialize<T>(MessagePath? path = null);
/// <summary>
/// Get the original string value
/// </summary>
/// <returns></returns>
string GetOriginalString();
}
/// <summary>
/// Stream message accessor
/// </summary>
public interface IStreamMessageAccessor : IMessageAccessor
{
/// <summary>
/// Load a stream message
/// </summary>
/// <param name="stream"></param>
/// <param name="bufferStream"></param>
Task<bool> Read(Stream stream, bool bufferStream);
}
/// <summary>
/// Byte message accessor
/// </summary>
public interface IByteMessageAccessor : IMessageAccessor
{
/// <summary>
/// Load a data message
/// </summary>
/// <param name="data"></param>
bool Read(ReadOnlyMemory<byte> data);
object Deserialize(Type type, MessagePath? path = null);
}
}
@@ -1,4 +1,4 @@
namespace CryptoExchange.Net.Interfaces
namespace CryptoExchange.Net.Sockets.MessageParsing.Interfaces
{
/// <summary>
/// Serializer interface
@@ -0,0 +1,158 @@
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace CryptoExchange.Net.Sockets.MessageParsing
{
/// <summary>
/// Json.Net message accessor
/// </summary>
public class JsonNetMessageAccessor : IMessageAccessor
{
private JToken? _token;
private Stream? _stream;
private static JsonSerializer _serializer = JsonSerializer.Create(SerializerOptions.WithConverters);
/// <inheritdoc />
public bool IsJson { get; private set; }
/// <inheritdoc />
public object? Underlying => _token;
/// <inheritdoc />
public void Load(Stream stream)
{
_stream = stream;
using var reader = new StreamReader(stream, Encoding.UTF8, false, (int)stream.Length, true);
using var jsonTextReader = new JsonTextReader(reader);
try
{
_token = JToken.Load(jsonTextReader);
IsJson = true;
}
catch (Exception)
{
// Not a json message
IsJson = false;
}
}
/// <inheritdoc />
public object Deserialize(Type type, MessagePath? path = null)
{
if (!IsJson)
{
var sr = new StreamReader(_stream);
return sr.ReadToEnd();
}
var source = _token;
if (path != null)
source = GetPathNode(path.Value);
return source!.ToObject(type, _serializer)!;
}
/// <inheritdoc />
public NodeType? GetNodeType()
{
if (_token == null)
return null;
if (_token.Type == JTokenType.Object)
return NodeType.Object;
if (_token.Type == JTokenType.Array)
return NodeType.Array;
return NodeType.Value;
}
/// <inheritdoc />
public NodeType? GetNodeType(MessagePath path)
{
var node = GetPathNode(path);
if (node == null)
return null;
if (node.Type == JTokenType.Object)
return NodeType.Object;
if (node.Type == JTokenType.Array)
return NodeType.Array;
return NodeType.Value;
}
/// <inheritdoc />
public T? GetValue<T>(MessagePath path)
{
var value = GetPathNode(path);
if (value == null)
return default;
if (value.Type == JTokenType.Object || value.Type == JTokenType.Array)
return default;
return value!.Value<T>();
}
/// <inheritdoc />
public List<T?>? GetValues<T>(MessagePath path)
{
var value = GetPathNode(path);
if (value == null)
return default;
if (value.Type == JTokenType.Object)
return default;
return value!.Values<T>().ToList();
}
private JToken? GetPathNode(MessagePath path)
{
var currentToken = _token;
foreach (var node in path)
{
if (node.Type == 0)
{
// Int value
var val = (int)node.Value!;
if (currentToken!.Type != JTokenType.Array || ((JArray)currentToken).Count <= val)
return null;
currentToken = currentToken[val];
}
else if (node.Type == 1)
{
// String value
if (currentToken!.Type != JTokenType.Object)
return null;
currentToken = currentToken[(string)node.Value!];
}
else
{
// Property name
if (currentToken!.Type != JTokenType.Object)
return null;
currentToken = (currentToken.First as JProperty)?.Name;
}
if (currentToken == null)
return null;
}
return currentToken;
}
}
}
@@ -1,10 +1,10 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
using Newtonsoft.Json;
namespace CryptoExchange.Net.Converters.JsonNet
namespace CryptoExchange.Net.Sockets.MessageParsing
{
/// <inheritdoc />
public class JsonNetMessageSerializer : IMessageSerializer
public class JsonNetSerializer : IMessageSerializer
{
/// <inheritdoc />
public string Serialize(object message) => JsonConvert.SerializeObject(message, Formatting.None);
@@ -1,4 +1,4 @@
namespace CryptoExchange.Net.Converters.MessageParsing
namespace CryptoExchange.Net.Sockets.MessageParsing
{
/// <summary>
/// Node accessor
@@ -6,23 +6,17 @@
public struct NodeAccessor
{
/// <summary>
/// Index
/// Value
/// </summary>
public int? Index { get; }
/// <summary>
/// Property name
/// </summary>
public string? Property { get; }
public object? Value { get; }
/// <summary>
/// Type (0 = int, 1 = string, 2 = prop name)
/// </summary>
public int Type { get; }
private NodeAccessor(int? index, string? property, int type)
private NodeAccessor(object? value, int type)
{
Index = index;
Property = property;
Value = value;
Type = type;
}
@@ -31,19 +25,20 @@
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static NodeAccessor Int(int value) { return new NodeAccessor(value, null, 0); }
public static NodeAccessor Int(int value) { return new NodeAccessor(value, 0); }
/// <summary>
/// Create a string node accessor
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static NodeAccessor String(string value) { return new NodeAccessor(null, value, 1); }
public static NodeAccessor String(string value) { return new NodeAccessor(value, 1); }
/// <summary>
/// Create a property name node accessor
/// </summary>
/// <returns></returns>
public static NodeAccessor PropertyName() { return new NodeAccessor(null, null, 2); }
public static NodeAccessor PropertyName() { return new NodeAccessor(null, 2); }
}
}
@@ -1,7 +1,7 @@
using System.Collections;
using System.Collections.Generic;
namespace CryptoExchange.Net.Converters.MessageParsing
namespace CryptoExchange.Net.Sockets.MessageParsing
{
/// <summary>
/// Message access definition
@@ -1,4 +1,4 @@
namespace CryptoExchange.Net.Converters.MessageParsing
namespace CryptoExchange.Net.Sockets.MessageParsing
{
/// <summary>
/// Message path extension methods
@@ -1,4 +1,4 @@
namespace CryptoExchange.Net.Converters.MessageParsing
namespace CryptoExchange.Net.Sockets.MessageParsing
{
/// <summary>
/// Message node type
+11 -15
View File
@@ -1,6 +1,7 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -19,11 +20,6 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public int Id { get; } = ExchangeHelpers.NextId();
/// <summary>
/// Can handle data
/// </summary>
public bool CanHandleData => true;
/// <summary>
/// Has this query been completed
/// </summary>
@@ -47,7 +43,7 @@ namespace CryptoExchange.Net.Sockets
/// <summary>
/// Wait event for the calling message processing thread
/// </summary>
public ManualResetEvent? ContinueAwaiter { get; set; }
public AsyncResetEvent? ContinueAwaiter { get; set; }
/// <summary>
/// Strings to match this query to a received message
@@ -120,7 +116,7 @@ namespace CryptoExchange.Net.Sockets
public async Task WaitAsync(TimeSpan timeout) => await _event.WaitAsync(timeout).ConfigureAwait(false);
/// <inheritdoc />
public virtual CallResult<object> Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type);
public virtual object Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type);
/// <summary>
/// Mark request as timeout
@@ -131,7 +127,7 @@ namespace CryptoExchange.Net.Sockets
/// Mark request as failed
/// </summary>
/// <param name="error"></param>
public abstract void Fail(Error error);
public abstract void Fail(string error);
/// <summary>
/// Handle a response message
@@ -139,7 +135,7 @@ namespace CryptoExchange.Net.Sockets
/// <param name="message"></param>
/// <param name="connection"></param>
/// <returns></returns>
public abstract CallResult Handle(SocketConnection connection, DataEvent<object> message);
public abstract Task<CallResult> HandleAsync(SocketConnection connection, DataEvent<object> message);
}
@@ -168,13 +164,13 @@ namespace CryptoExchange.Net.Sockets
}
/// <inheritdoc />
public override CallResult Handle(SocketConnection connection, DataEvent<object> message)
public override async Task<CallResult> HandleAsync(SocketConnection connection, DataEvent<object> message)
{
Completed = true;
Response = message.Data;
Result = HandleMessage(connection, message.As((TResponse)message.Data));
Result = await HandleMessageAsync(connection, message.As((TResponse)message.Data)).ConfigureAwait(false);
_event.Set();
ContinueAwaiter?.WaitOne();
await (ContinueAwaiter?.WaitAsync() ?? Task.CompletedTask).ConfigureAwait(false);
return Result;
}
@@ -184,7 +180,7 @@ namespace CryptoExchange.Net.Sockets
/// <param name="connection"></param>
/// <param name="message"></param>
/// <returns></returns>
public virtual CallResult<TResponse> HandleMessage(SocketConnection connection, DataEvent<TResponse> message) => new CallResult<TResponse>(message.Data, message.OriginalData, null);
public virtual Task<CallResult<TResponse>> HandleMessageAsync(SocketConnection connection, DataEvent<TResponse> message) => Task.FromResult(new CallResult<TResponse>(message.Data, message.OriginalData, null));
/// <inheritdoc />
public override void Timeout()
@@ -199,9 +195,9 @@ namespace CryptoExchange.Net.Sockets
}
/// <inheritdoc />
public override void Fail(Error error)
public override void Fail(string error)
{
Result = new CallResult<TResponse>(error);
Result = new CallResult<TResponse>(new ServerError(error));
Completed = true;
ContinueAwaiter?.Set();
_event.Set();
+136 -158
View File
@@ -6,11 +6,12 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using CryptoExchange.Net.Objects;
using System.Net.WebSockets;
using System.IO;
using CryptoExchange.Net.Objects.Sockets;
using System.Text;
using System.Diagnostics;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Logging.Extensions;
using System.Threading;
using CryptoExchange.Net.Sockets.MessageParsing;
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
namespace CryptoExchange.Net.Sockets
{
@@ -129,7 +130,7 @@ namespace CryptoExchange.Net.Sockets
if (_pausedActivity != value)
{
_pausedActivity = value;
_logger.ActivityPaused(SocketId, value);
_logger.Log(LogLevel.Information, $"[Sckt {SocketId}] paused activity: " + value);
if(_pausedActivity) _ = Task.Run(() => ActivityPaused?.Invoke());
else _ = Task.Run(() => ActivityUnpaused?.Invoke());
}
@@ -149,7 +150,7 @@ namespace CryptoExchange.Net.Sockets
var oldStatus = _status;
_status = value;
_logger.SocketStatusChanged(SocketId, oldStatus, value);
_logger.Log(LogLevel.Debug, $"[Sckt {SocketId}] status changed from {oldStatus} to {_status}");
}
}
@@ -159,8 +160,8 @@ namespace CryptoExchange.Net.Sockets
private readonly ILogger _logger;
private SocketStatus _status;
private readonly IMessageSerializer _serializer;
private readonly IByteMessageAccessor _accessor;
private IMessageSerializer _serializer;
private IMessageAccessor _accessor;
/// <summary>
/// The task that is sending periodic data on the websocket. Can be used for sending Ping messages every x seconds or similair. Not necesarry.
@@ -204,8 +205,8 @@ namespace CryptoExchange.Net.Sockets
_listenersLock = new object();
_listeners = new List<IMessageProcessor>();
_serializer = apiClient.CreateSerializer();
_accessor = apiClient.CreateAccessor();
_serializer = new JsonNetSerializer();
_accessor = new JsonNetMessageAccessor();
}
/// <summary>
@@ -233,7 +234,7 @@ namespace CryptoExchange.Net.Sockets
foreach (var query in _listeners.OfType<Query>().ToList())
{
query.Fail(new WebError("Connection interupted"));
query.Fail("Connection interupted");
_listeners.Remove(query);
}
}
@@ -258,7 +259,7 @@ namespace CryptoExchange.Net.Sockets
foreach (var query in _listeners.OfType<Query>().ToList())
{
query.Fail(new WebError("Connection interupted"));
query.Fail("Connection interupted");
_listeners.Remove(query);
}
}
@@ -287,7 +288,7 @@ namespace CryptoExchange.Net.Sockets
{
foreach (var query in _listeners.OfType<Query>().ToList())
{
query.Fail(new WebError("Connection interupted"));
query.Fail("Connection interupted");
_listeners.Remove(query);
}
}
@@ -295,29 +296,21 @@ namespace CryptoExchange.Net.Sockets
// Can't wait for this as it would cause a deadlock
_ = Task.Run(async () =>
{
try
var reconnectSuccessful = await ProcessReconnectAsync().ConfigureAwait(false);
if (!reconnectSuccessful)
{
var reconnectSuccessful = await ProcessReconnectAsync().ConfigureAwait(false);
if (!reconnectSuccessful)
{
_logger.FailedReconnectProcessing(SocketId, reconnectSuccessful.Error?.ToString());
_ = _socket.ReconnectAsync().ConfigureAwait(false);
}
else
{
Status = SocketStatus.Connected;
_ = Task.Run(() =>
{
ConnectionRestored?.Invoke(DateTime.UtcNow - DisconnectTime!.Value);
DisconnectTime = null;
});
}
}
catch(Exception ex)
{
_logger.UnkownExceptionWhileProcessingReconnection(SocketId, ex);
_logger.Log(LogLevel.Warning, $"[Sckt {SocketId}] failed reconnect processing: {reconnectSuccessful.Error}, reconnecting again");
_ = _socket.ReconnectAsync().ConfigureAwait(false);
}
else
{
Status = SocketStatus.Connected;
_ = Task.Run(() =>
{
ConnectionRestored?.Invoke(DateTime.UtcNow - DisconnectTime!.Value);
DisconnectTime = null;
});
}
});
return Task.CompletedTask;
@@ -330,9 +323,9 @@ namespace CryptoExchange.Net.Sockets
protected virtual Task HandleErrorAsync(Exception e)
{
if (e is WebSocketException wse)
_logger.WebSocketErrorCodeAndDetails(SocketId, wse.WebSocketErrorCode, wse.Message, wse);
_logger.Log(LogLevel.Warning, $"[Sckt {SocketId}] error: Websocket error code {wse.WebSocketErrorCode}, details: " + e.ToLogString());
else
_logger.WebSocketError(SocketId, e.Message, e);
_logger.Log(LogLevel.Warning, $"[Sckt {SocketId}] error: " + e.ToLogString());
return Task.CompletedTask;
}
@@ -351,7 +344,7 @@ namespace CryptoExchange.Net.Sockets
if (query == null)
{
_logger.MessageSentNotPending(SocketId, requestId);
_logger.Log(LogLevel.Debug, $"[Sckt {SocketId}] msg {requestId} - message sent, but not pending");
return Task.CompletedTask;
}
@@ -362,116 +355,112 @@ namespace CryptoExchange.Net.Sockets
/// <summary>
/// Handle a message
/// </summary>
/// <param name="data"></param>
/// <param name="stream"></param>
/// <param name="type"></param>
/// <returns></returns>
protected virtual void HandleStreamMessage(WebSocketMessageType type, ReadOnlyMemory<byte> data)
protected virtual async Task HandleStreamMessage(WebSocketMessageType type, Stream stream)
{
var sw = Stopwatch.StartNew();
var receiveTime = DateTime.UtcNow;
string? originalData = null;
// 1. Decrypt/Preprocess if necessary
data = ApiClient.PreprocessStreamMessage(type, data);
stream = ApiClient.PreprocessStreamMessage(type, stream);
// 2. Read data into accessor
_accessor.Read(data);
try
_accessor.Load(stream);
if (ApiClient.ApiOptions.OutputOriginalData ?? ApiClient.ClientOptions.OutputOriginalData)
{
bool outputOriginalData = ApiClient.ApiOptions.OutputOriginalData ?? ApiClient.ClientOptions.OutputOriginalData;
if (outputOriginalData)
{
originalData = _accessor.GetOriginalString();
_logger.ReceivedData(SocketId, originalData);
}
stream.Position = 0;
using var textReader = new StreamReader(stream, Encoding.UTF8, false, 1024, true);
originalData = textReader.ReadToEnd();
// 3. Determine the identifying properties of this message
var listenId = ApiClient.GetListenerIdentifier(_accessor);
if (listenId == null)
{
originalData = outputOriginalData ? _accessor.GetOriginalString() : "[OutputOriginalData is false]";
if (!ApiClient.UnhandledMessageExpected)
_logger.FailedToEvaluateMessage(SocketId, originalData);
_logger.LogTrace("[Sckt {SocketId}] received {Data}", SocketId, originalData);
}
// 3. Determine the identifying properties of this message
var listenId = ApiClient.GetListenerIdentifier(_accessor);
if (listenId == null)
{
if (!ApiClient.UnhandledMessageExpected)
_logger.LogWarning("[Sckt {SocketId}] failed to evaluate message", SocketId);
UnhandledMessage?.Invoke(_accessor);
stream.Dispose();
return;
}
// 4. Get the listeners interested in this message
List<IMessageProcessor> processors;
lock(_listenersLock)
processors = _listeners.Where(s => s.ListenerIdentifiers.Contains(listenId)).ToList();
if (!processors.Any())
{
if (!ApiClient.UnhandledMessageExpected)
{
_logger.LogWarning("[Sckt {SocketId}] received message not matched to any listener. ListenId: {ListenId}", SocketId, listenId);
UnhandledMessage?.Invoke(_accessor);
return;
}
// 4. Get the listeners interested in this message
List<IMessageProcessor> processors;
lock (_listenersLock)
processors = _listeners.Where(s => s.ListenerIdentifiers.Contains(listenId) && s.CanHandleData).ToList();
stream.Dispose();
return;
}
if (processors.Count == 0)
_logger.LogTrace("[Sckt {SocketId}] {Count} processor(s) matched to message with listener identifier {ListenerId}", SocketId, processors.Count, listenId);
var totalUserTime = 0;
Dictionary<Type, object>? desCache = null;
if (processors.Count > 1)
{
// Only instantiate a cache if there are multiple processors
desCache = new Dictionary<Type, object>();
}
foreach (var processor in processors)
{
// 5. Determine the type to deserialize to for this processor
var messageType = processor.GetMessageType(_accessor);
if (messageType == null)
{
if (!ApiClient.UnhandledMessageExpected)
{
List<string> listenerIds;
lock (_listenersLock)
listenerIds = _listeners.Where(l => l.CanHandleData).SelectMany(l => l.ListenerIdentifiers).ToList();
_logger.ReceivedMessageNotMatchedToAnyListener(SocketId, listenId, string.Join(",", listenerIds));
UnhandledMessage?.Invoke(_accessor);
}
return;
_logger.LogWarning("[Sckt {SocketId}] received message not recognized by handler {Id}", SocketId, processor.Id);
continue;
}
_logger.ProcessorMatched(SocketId, processors.Count, listenId);
var totalUserTime = 0;
Dictionary<Type, object>? desCache = null;
if (processors.Count > 1)
// 6. Deserialize the message
object? deserialized = null;
desCache?.TryGetValue(messageType, out deserialized);
if (deserialized == null)
{
// Only instantiate a cache if there are multiple processors
desCache = new Dictionary<Type, object>();
}
foreach (var processor in processors)
{
// 5. Determine the type to deserialize to for this processor
var messageType = processor.GetMessageType(_accessor);
if (messageType == null)
{
_logger.ReceivedMessageNotRecognized(SocketId, processor.Id);
continue;
}
// 6. Deserialize the message
object? deserialized = null;
desCache?.TryGetValue(messageType, out deserialized);
if (deserialized == null)
{
var desResult = processor.Deserialize(_accessor, messageType);
if (!desResult)
{
_logger.FailedToDeserializeMessage(SocketId, desResult.Error?.ToString());
continue;
}
deserialized = desResult.Data;
desCache?.Add(messageType, deserialized);
}
// 7. Hand of the message to the subscription
try
{
var innerSw = Stopwatch.StartNew();
processor.Handle(this, new DataEvent<object>(deserialized, null, originalData, receiveTime, null));
totalUserTime += (int)innerSw.ElapsedMilliseconds;
deserialized = processor.Deserialize(_accessor, messageType);
desCache?.Add(messageType, deserialized);
}
catch (Exception ex)
{
_logger.UserMessageProcessingFailed(SocketId, ex.ToLogString(), ex);
if (processor is Subscription subscription)
subscription.InvokeExceptionHandler(ex);
_logger.LogWarning("[Sckt {SocketId}] failed to deserialize message to type {Type}: {Exception}", SocketId, messageType.Name, ex.ToLogString());
continue;
}
}
_logger.MessageProcessed(SocketId, sw.ElapsedMilliseconds, sw.ElapsedMilliseconds - totalUserTime);
}
finally
{
_accessor.Clear();
// 7. Hand of the message to the subscription
try
{
var innerSw = Stopwatch.StartNew();
await processor.HandleAsync(this, new DataEvent<object>(deserialized, null, originalData, receiveTime, null)).ConfigureAwait(false);
totalUserTime += (int)innerSw.ElapsedMilliseconds;
}
catch (Exception ex)
{
_logger.LogWarning("[Sckt {SocketId}] user message processing failed: {Exception}", SocketId, ex.ToLogString());
if (processor is Subscription subscription)
subscription.InvokeExceptionHandler(ex);
}
}
stream.Dispose();
_logger.LogTrace($"[Sckt {SocketId}] message processed in {(int)sw.ElapsedMilliseconds}ms ({sw.ElapsedMilliseconds - totalUserTime}ms parsing)");
}
/// <summary>
@@ -530,7 +519,7 @@ namespace CryptoExchange.Net.Sockets
if (Status == SocketStatus.Closing || Status == SocketStatus.Closed || Status == SocketStatus.Disposed)
return;
_logger.ClosingSubscription(SocketId, subscription.Id);
_logger.Log(LogLevel.Debug, $"[Sckt {SocketId}] closing subscription {subscription.Id}");
if (subscription.CancellationTokenRegistration.HasValue)
subscription.CancellationTokenRegistration.Value.Dispose();
@@ -549,12 +538,12 @@ namespace CryptoExchange.Net.Sockets
}
else
{
_logger.NotUnsubscribingSubscriptionBecauseDuplicateRunning(SocketId);
_logger.Log(LogLevel.Debug, $"[Sckt {SocketId}] not unsubscribing subscription as there is still a duplicate subscription running");
}
if (Status == SocketStatus.Closing)
{
_logger.AlreadyClosing(SocketId);
_logger.Log(LogLevel.Debug, $"[Sckt {SocketId}] already closing");
return;
}
@@ -568,7 +557,7 @@ namespace CryptoExchange.Net.Sockets
if (shouldCloseConnection)
{
_logger.ClosingNoMoreSubscriptions(SocketId);
_logger.Log(LogLevel.Debug, $"[Sckt {SocketId}] closing as there are no more subscriptions");
await CloseAsync().ConfigureAwait(false);
}
@@ -606,7 +595,7 @@ namespace CryptoExchange.Net.Sockets
_listeners.Add(subscription);
if (subscription.UserSubscription)
_logger.AddingNewSubscription(SocketId, subscription.Id, UserSubscriptionCount);
_logger.Log(LogLevel.Debug, $"[Sckt {SocketId}] adding new subscription with id {subscription.Id}, total subscriptions on connection: {UserSubscriptionCount}");
return true;
}
@@ -626,7 +615,7 @@ namespace CryptoExchange.Net.Sockets
/// <param name="query">Query to send</param>
/// <param name="continueEvent">Wait event for when the socket message handler can continue</param>
/// <returns></returns>
public virtual async Task<CallResult> SendAndWaitQueryAsync(Query query, ManualResetEvent? continueEvent = null)
public virtual async Task<CallResult> SendAndWaitQueryAsync(Query query, AsyncResetEvent? continueEvent = null)
{
await SendAndWaitIntAsync(query, continueEvent).ConfigureAwait(false);
return query.Result ?? new CallResult(new ServerError("Timeout"));
@@ -639,22 +628,22 @@ namespace CryptoExchange.Net.Sockets
/// <param name="query">Query to send</param>
/// <param name="continueEvent">Wait event for when the socket message handler can continue</param>
/// <returns></returns>
public virtual async Task<CallResult<T>> SendAndWaitQueryAsync<T>(Query<T> query, ManualResetEvent? continueEvent = null)
public virtual async Task<CallResult<T>> SendAndWaitQueryAsync<T>(Query<T> query, AsyncResetEvent? continueEvent = null)
{
await SendAndWaitIntAsync(query, continueEvent).ConfigureAwait(false);
return query.TypedResult ?? new CallResult<T>(new ServerError("Timeout"));
}
private async Task SendAndWaitIntAsync(Query query, ManualResetEvent? continueEvent)
private async Task SendAndWaitIntAsync(Query query, AsyncResetEvent? continueEvent)
{
lock(_listenersLock)
_listeners.Add(query);
query.ContinueAwaiter = continueEvent;
var sendResult = Send(query.Id, query.Request, query.Weight);
if (!sendResult)
var sendOk = Send(query.Id, query.Request, query.Weight);
if (!sendOk)
{
query.Fail(sendResult.Error!);
query.Fail("Failed to send");
lock (_listenersLock)
_listeners.Remove(query);
return;
@@ -666,7 +655,7 @@ namespace CryptoExchange.Net.Sockets
{
if (!_socket.IsOpen)
{
query.Fail(new WebError("Socket not open"));
query.Fail("Socket not open");
return;
}
@@ -693,10 +682,12 @@ namespace CryptoExchange.Net.Sockets
/// <param name="requestId">The request id</param>
/// <param name="obj">The object to send</param>
/// <param name="weight">The weight of the message</param>
public virtual CallResult Send<T>(int requestId, T obj, int weight)
public virtual bool Send<T>(int requestId, T obj, int weight)
{
var data = obj is string str ? str : _serializer.Serialize(obj!);
return Send(requestId, data, weight);
if(obj is string str)
return Send(requestId, str, weight);
else
return Send(requestId, _serializer.Serialize(obj!), weight);
}
/// <summary>
@@ -705,30 +696,17 @@ namespace CryptoExchange.Net.Sockets
/// <param name="data">The data to send</param>
/// <param name="weight">The weight of the message</param>
/// <param name="requestId">The id of the request</param>
public virtual CallResult Send(int requestId, string data, int weight)
public virtual bool Send(int requestId, string data, int weight)
{
if (ApiClient.MessageSendSizeLimit != null && data.Length > ApiClient.MessageSendSizeLimit.Value)
{
var info = $"Message to send exceeds the max server message size ({ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit";
_logger.LogWarning("[Sckt {SocketId}] msg {RequestId} - {Info}", SocketId, requestId, info);
return new CallResult(new InvalidOperationError(info));
}
if (!_socket.IsOpen)
{
_logger.LogWarning("[Sckt {SocketId}] msg {RequestId} - Failed to send, socket no longer open", SocketId, requestId);
return new CallResult(new WebError("Failed to send message, socket no longer open"));
}
_logger.SendingData(SocketId, requestId, data);
_logger.Log(LogLevel.Trace, $"[Sckt {SocketId}] msg {requestId} - sending messsage: {data}");
try
{
_socket.Send(requestId, data, weight);
return new CallResult(null);
return true;
}
catch(Exception ex)
catch(Exception)
{
return new CallResult(new WebError("Failed to send message: " + ex.Message));
return false;
}
}
@@ -743,7 +721,7 @@ namespace CryptoExchange.Net.Sockets
if (!anySubscriptions)
{
// No need to resubscribe anything
_logger.NothingToResubscribeCloseConnection(SocketId);
_logger.Log(LogLevel.Debug, $"[Sckt {SocketId}] nothing to resubscribe, closing connection");
_ = _socket.CloseAsync();
return new CallResult<bool>(true);
}
@@ -757,12 +735,12 @@ namespace CryptoExchange.Net.Sockets
var authResult = await ApiClient.AuthenticateSocketAsync(this).ConfigureAwait(false);
if (!authResult)
{
_logger.FailedAuthenticationDisconnectAndRecoonect(SocketId);
_logger.Log(LogLevel.Warning, $"[Sckt {SocketId}] authentication failed on reconnected socket. Disconnecting and reconnecting.");
return authResult;
}
Authenticated = true;
_logger.AuthenticationSucceeded(SocketId);
_logger.Log(LogLevel.Debug, $"[Sckt {SocketId}] authentication succeeded on reconnected socket.");
}
// Get a list of all subscriptions on the socket
@@ -776,8 +754,8 @@ namespace CryptoExchange.Net.Sockets
var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false);
if (!result)
{
_logger.FailedRequestRevitalization(SocketId, result.Error?.ToString());
return result;
_logger.Log(LogLevel.Warning, $"[Sckt {SocketId}] failed request revitalization: " + result.Error);
return result.As(false);
}
}
@@ -794,7 +772,7 @@ namespace CryptoExchange.Net.Sockets
if (subQuery == null)
continue;
var waitEvent = new ManualResetEvent(false);
var waitEvent = new AsyncResetEvent(false);
taskList.Add(SendAndWaitQueryAsync(subQuery, waitEvent).ContinueWith((r) =>
{
subscription.HandleSubQueryResponse(subQuery.Response!);
@@ -814,7 +792,7 @@ namespace CryptoExchange.Net.Sockets
if (!_socket.IsOpen)
return new CallResult<bool>(new WebError("Socket not connected"));
_logger.AllSubscriptionResubscribed(SocketId);
_logger.Log(LogLevel.Debug, $"[Sckt {SocketId}] all subscription successfully resubscribed on reconnected socket.");
return new CallResult<bool>(true);
}
@@ -825,7 +803,7 @@ namespace CryptoExchange.Net.Sockets
return;
await SendAndWaitQueryAsync(unsubscribeRequest).ConfigureAwait(false);
_logger.SubscriptionUnsubscribed(SocketId, subscription.Id);
_logger.Log(LogLevel.Information, $"[Sckt {SocketId}] subscription {subscription!.Id} unsubscribed");
}
internal async Task<CallResult> ResubscribeAsync(Subscription subscription)
@@ -876,7 +854,7 @@ namespace CryptoExchange.Net.Sockets
if (query == null)
continue;
_logger.SendingPeriodic(SocketId, identifier);
_logger.Log(LogLevel.Trace, $"[Sckt {SocketId}] sending periodic {identifier}");
try
{
@@ -885,7 +863,7 @@ namespace CryptoExchange.Net.Sockets
}
catch (Exception ex)
{
_logger.PeriodicSendFailed(SocketId, identifier, ex.ToLogString(), ex);
_logger.Log(LogLevel.Warning, $"[Sckt {SocketId}] Periodic send {identifier} failed: " + ex.ToLogString());
}
}
});
+5 -14
View File
@@ -1,6 +1,7 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
@@ -19,11 +20,6 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public int Id { get; set; }
/// <summary>
/// Can handle data
/// </summary>
public bool CanHandleData => Confirmed || HandleUpdatesBeforeConfirmation;
/// <summary>
/// Total amount of invocations
/// </summary>
@@ -44,11 +40,6 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public bool Confirmed { get; set; }
/// <summary>
/// Whether this subscription should handle update messages before confirmation
/// </summary>
public bool HandleUpdatesBeforeConfirmation { get; set; }
/// <summary>
/// Is the subscription closed
/// </summary>
@@ -125,7 +116,7 @@ namespace CryptoExchange.Net.Sockets
public abstract Query? GetUnsubQuery();
/// <inheritdoc />
public virtual CallResult<object> Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type);
public virtual object Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type);
/// <summary>
/// Handle an update message
@@ -133,11 +124,11 @@ namespace CryptoExchange.Net.Sockets
/// <param name="connection"></param>
/// <param name="message"></param>
/// <returns></returns>
public CallResult Handle(SocketConnection connection, DataEvent<object> message)
public async Task<CallResult> HandleAsync(SocketConnection connection, DataEvent<object> message)
{
ConnectionInvocations++;
TotalInvocations++;
return DoHandleMessage(connection, message);
return await DoHandleMessageAsync(connection, message).ConfigureAwait(false);
}
/// <summary>
@@ -146,7 +137,7 @@ namespace CryptoExchange.Net.Sockets
/// <param name="connection"></param>
/// <param name="message"></param>
/// <returns></returns>
public abstract CallResult DoHandleMessage(SocketConnection connection, DataEvent<object> message);
public abstract Task<CallResult> DoHandleMessageAsync(SocketConnection connection, DataEvent<object> message);
/// <summary>
/// Invoke the exception event
@@ -1,6 +1,6 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
@@ -19,7 +19,6 @@ namespace CryptoExchange.Net.Sockets
/// <param name="authenticated"></param>
public SystemSubscription(ILogger logger, bool authenticated = false) : base(logger, authenticated, false)
{
Confirmed = true;
}
/// <inheritdoc />
@@ -36,8 +35,8 @@ namespace CryptoExchange.Net.Sockets
public override Type GetMessageType(IMessageAccessor message) => typeof(T);
/// <inheritdoc />
public override CallResult DoHandleMessage(SocketConnection connection, DataEvent<object> message)
=> HandleMessage(connection, message.As((T)message.Data));
public override Task<CallResult> DoHandleMessageAsync(SocketConnection connection, DataEvent<object> message)
=> HandleMessageAsync(connection, message.As((T)message.Data));
/// <summary>
/// ctor
@@ -54,6 +53,6 @@ namespace CryptoExchange.Net.Sockets
/// <param name="connection"></param>
/// <param name="message"></param>
/// <returns></returns>
public abstract CallResult HandleMessage(SocketConnection connection, DataEvent<T> message);
public abstract Task<CallResult> HandleMessageAsync(SocketConnection connection, DataEvent<T> message);
}
}
+10 -10
View File
@@ -5,16 +5,16 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Binance.Net" Version="9.7.1" />
<PackageReference Include="Bitfinex.Net" Version="7.2.2" />
<PackageReference Include="Bybit.Net" Version="3.7.1" />
<PackageReference Include="CoinEx.Net" Version="6.2.1" />
<PackageReference Include="Huobi.Net" Version="5.2.1" />
<PackageReference Include="JK.BingX.Net" Version="1.0.0" />
<PackageReference Include="JK.Bitget.Net" Version="1.3.1" />
<PackageReference Include="JK.OKX.Net" Version="1.7.1" />
<PackageReference Include="KrakenExchange.Net" Version="4.4.3" />
<PackageReference Include="Kucoin.Net" Version="5.3.2" />
<PackageReference Include="Binance.Net" Version="9.1.5" />
<PackageReference Include="Bitfinex.Net" Version="7.0.4" />
<PackageReference Include="Bittrex.Net" Version="8.0.3" />
<PackageReference Include="Bybit.Net" Version="3.2.1" />
<PackageReference Include="CoinEx.Net" Version="6.0.3" />
<PackageReference Include="Huobi.Net" Version="5.0.3" />
<PackageReference Include="JK.Bitget.Net" Version="1.0.0" />
<PackageReference Include="JK.OKX.Net" Version="1.4.2" />
<PackageReference Include="KrakenExchange.Net" Version="4.1.5" />
<PackageReference Include="Kucoin.Net" Version="5.0.5" />
<PackageReference Include="Serilog.AspNetCore" Version="6.0.0" />
</ItemGroup>
+6 -6
View File
@@ -1,8 +1,8 @@
@page "/"
@inject IBinanceRestClient binanceClient
@inject IBingXRestClient bingXClient
@inject IBitfinexRestClient bitfinexClient
@inject IBitgetRestClient bitgetClient
@inject IBittrexRestClient bittrexClient
@inject IBybitRestClient bybitClient
@inject ICoinExRestClient coinexClient
@inject IHuobiRestClient huobiClient
@@ -22,9 +22,9 @@
protected override async Task OnInitializedAsync()
{
var binanceTask = binanceClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
var bingXTask = bingXClient.SpotApi.ExchangeData.GetTickersAsync("BTC-USDT");
var bitfinexTask = bitfinexClient.SpotApi.ExchangeData.GetTickerAsync("tBTCUSD");
var bitgetTask = bitgetClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT_SPBL");
var bittrexTask = bittrexClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
var bybitTask = bybitClient.V5Api.ExchangeData.GetSpotTickersAsync("BTCUSDT");
var coinexTask = coinexClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
var huobiTask = huobiClient.SpotApi.ExchangeData.GetTickerAsync("btcusdt");
@@ -32,13 +32,10 @@
var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
var okxTask = okxClient.UnifiedApi.ExchangeData.GetTickerAsync("BTCUSDT");
await Task.WhenAll(binanceTask, bingXTask, bitfinexTask, bybitTask, coinexTask, huobiTask, krakenTask, kucoinTask);
await Task.WhenAll(binanceTask, bitfinexTask, bittrexTask, bybitTask, coinexTask, huobiTask, krakenTask, kucoinTask);
if (binanceTask.Result.Success)
_prices.Add("Binance", binanceTask.Result.Data.LastPrice);
if (bingXTask.Result.Success)
_prices.Add("BingX", bingXTask.Result.Data.First().LastPrice);
if (bitfinexTask.Result.Success)
_prices.Add("Bitfinex", bitfinexTask.Result.Data.LastPrice);
@@ -46,6 +43,9 @@
if (bitgetTask.Result.Success)
_prices.Add("Bitget", bitgetTask.Result.Data.ClosePrice);
if (bittrexTask.Result.Success)
_prices.Add("Bittrex", bittrexTask.Result.Data.LastPrice);
if (bybitTask.Result.Success)
_prices.Add("Bybit", bybitTask.Result.Data.List.First().LastPrice);
+3 -4
View File
@@ -1,8 +1,8 @@
@page "/LiveData"
@inject IBinanceSocketClient binanceSocketClient
@inject IBingXSocketClient bingXSocketClient
@inject IBitfinexSocketClient bitfinexSocketClient
@inject IBitgetSocketClient bitgetSocketClient
@inject IBittrexSocketClient bittrexSocketClient
@inject IBybitSocketClient bybitSocketClient
@inject ICoinExSocketClient coinExSocketClient
@inject IHuobiSocketClient huobiSocketClient
@@ -11,7 +11,6 @@
@inject IOKXSocketClient okxSocketClient
@using System.Collections.Concurrent
@using CryptoExchange.Net.Objects
@using CryptoExchange.Net.Objects.Sockets;
@using CryptoExchange.Net.Sockets
@implements IDisposable
@@ -30,15 +29,15 @@
var tasks = new Task<CallResult<UpdateSubscription>>[]
{
binanceSocketClient.SpotApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Binance", data.Data.LastPrice)),
bingXSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("BingX", data.Data.LastPrice)),
bitfinexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)),
bitgetSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bitget", data.Data.LastPrice)),
bittrexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Bittrex", data.Data.LastPrice)),
bybitSocketClient.V5SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)),
coinExSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("CoinEx", data.Data.LastPrice)),
huobiSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ethbtc", data => UpdateData("Huobi", data.Data.ClosePrice ?? 0)),
krakenSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH/XBT", data => UpdateData("Kraken", data.Data.LastTrade.Price)),
kucoinSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)),
okxSocketClient.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("OKX", data.Data.LastPrice ?? 0)),
okxSocketClient.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("OKX", data.LastPrice ?? 0)),
};
await Task.WhenAll(tasks);
+3 -3
View File
@@ -2,9 +2,9 @@
@using System.Collections.Concurrent
@using System.Timers
@using Binance.Net.Interfaces
@using BingX.Net.Interfaces
@using Bitfinex.Net.Interfaces
@using Bitget.Net.Interfaces;
@using Bittrex.Net.Interfaces
@using Bybit.Net.Interfaces
@using CoinEx.Net.Interfaces
@using CryptoExchange.Net.Interfaces
@@ -14,9 +14,9 @@
@using Kucoin.Net.Interfaces
@using OKX.Net.Interfaces;
@inject IBinanceOrderBookFactory binanceFactory
@inject IBingXOrderBookFactory bingXFactory
@inject IBitfinexOrderBookFactory bitfinexFactory
@inject IBitgetOrderBookFactory bitgetFactory
@inject IBittrexOrderBookFactory bittrexFactory
@inject IBybitOrderBookFactory bybitFactory
@inject ICoinExOrderBookFactory coinExFactory
@inject IHuobiOrderBookFactory huobiFactory
@@ -57,9 +57,9 @@
_books = new Dictionary<string, ISymbolOrderBook>
{
{ "Binance", binanceFactory.CreateSpot("ETHBTC") },
{ "BingX", bingXFactory.CreateSpot("ETH-BTC") },
{ "Bitfinex", bitfinexFactory.Create("tETHBTC") },
{ "Bitget", bitgetFactory.CreateSpot("ETHBTC") },
{ "Bittrex", bittrexFactory.Create("ETH-BTC") },
{ "Bybit", bybitFactory.Create("ETHBTC", Bybit.Net.Enums.Category.Spot) },
{ "CoinEx", coinExFactory.CreateSpot("ETHBTC") },
{ "Huobi", huobiFactory.CreateSpot("ethbtc") },
+37 -2
View File
@@ -1,5 +1,26 @@
@page "/SpotClient"
@inject ICryptoRestClient restClient
@inject IBinanceRestClient binanceClient
@inject IBitfinexRestClient bitfinexClient
@inject IBitgetRestClient bitgetClient
@inject IBittrexRestClient bittrexClient
@inject IBybitRestClient bybitClient
@inject ICoinExRestClient coinexClient
@inject IHuobiRestClient huobiClient
@inject IKrakenRestClient krakenClient
@inject IKucoinRestClient kucoinClient
@inject IOKXRestClient okxClient
@using Binance.Net.Clients.SpotApi
@using Bitfinex.Net.Clients.SpotApi
@using Bittrex.Net.Clients.SpotApi
@using Bitget.Net.Clients.SpotApi
@using Bybit.Net.Clients.SpotApi
@using CoinEx.Net.Clients.SpotApi
@using CryptoExchange.Net.Interfaces
@using CryptoExchange.Net.Interfaces.CommonClients
@using Huobi.Net.Clients.SpotApi
@using Kraken.Net.Clients.SpotApi
@using Kucoin.Net.Clients.SpotApi
@using OKX.Net.Clients.UnifiedApi
<h3>ETH-BTC prices:</h3>
@foreach(var price in _prices.OrderBy(p => p.Key))
@@ -12,7 +33,21 @@
protected override async Task OnInitializedAsync()
{
var clients = restClient.GetSpotClients();
var clients = new ISpotClient[]
{
binanceClient.SpotApi.CommonSpotClient,
bitfinexClient.SpotApi.CommonSpotClient,
bitgetClient.SpotApi.CommonSpotClient,
bittrexClient.SpotApi.CommonSpotClient,
bybitClient.SpotApiV1.CommonSpotClient,
coinexClient.SpotApi.CommonSpotClient,
huobiClient.SpotApi.CommonSpotClient,
krakenClient.SpotApi.CommonSpotClient,
kucoinClient.SpotApi.CommonSpotClient,
okxClient.UnifiedApi.CommonSpotClient
};
var tasks = clients.Select(c => (c.ExchangeName, c.GetTickerAsync(c.GetSymbolName("ETH", "BTC"))));
await Task.WhenAll(tasks.Select(t => t.Item2));
foreach(var task in tasks)
+14 -1
View File
@@ -1,10 +1,23 @@
using System.Collections.Generic;
using Binance.Net;
using Binance.Net.Clients;
using Binance.Net.Interfaces.Clients;
using Bitfinex.Net;
using Bitget.Net;
using Bittrex.Net;
using Bybit.Net;
using CoinEx.Net;
using CryptoExchange.Net.Authentication;
using Huobi.Net;
using Kraken.Net;
using Kucoin.Net;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using OKX.Net;
namespace BlazorClient
{
@@ -36,9 +49,9 @@ namespace BlazorClient
socketOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
});
services.AddBingX();
services.AddBitfinex();
services.AddBitget();
services.AddBittrex();
services.AddBybit();
services.AddCoinEx();
services.AddHuobi();
+2 -3
View File
@@ -9,13 +9,12 @@
@using BlazorClient
@using BlazorClient.Shared
@using Binance.Net.Interfaces.Clients;
@using BingX.Net.Interfaces.Clients;
@using Bitfinex.Net.Interfaces.Clients;
@using Bitget.Net.Interfaces.Clients;
@using Bittrex.Net.Interfaces.Clients;
@using Bybit.Net.Interfaces.Clients;
@using CoinEx.Net.Interfaces.Clients;
@using Huobi.Net.Interfaces.Clients;
@using Kraken.Net.Interfaces.Clients;
@using Kucoin.Net.Interfaces.Clients;
@using OKX.Net.Interfaces.Clients;
@using CryptoExchange.Net.Interfaces;
@using OKX.Net.Interfaces.Clients;
+9 -9
View File
@@ -6,16 +6,16 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Binance.Net" Version="9.5.0" />
<PackageReference Include="Bitfinex.Net" Version="7.1.0" />
<PackageReference Include="Binance.Net" Version="9.1.5" />
<PackageReference Include="Bitfinex.Net" Version="7.0.4" />
<PackageReference Include="Bittrex.Net" Version="8.0.3" />
<PackageReference Include="Bybit.Net" Version="3.4.0" />
<PackageReference Include="CoinEx.Net" Version="6.1.0" />
<PackageReference Include="Huobi.Net" Version="5.1.0" />
<PackageReference Include="JK.Bitget.Net" Version="1.1.0" />
<PackageReference Include="JK.OKX.Net" Version="1.6.0" />
<PackageReference Include="KrakenExchange.Net" Version="4.3.0" />
<PackageReference Include="Kucoin.Net" Version="5.2.0" />
<PackageReference Include="Bybit.Net" Version="3.2.1" />
<PackageReference Include="CoinEx.Net" Version="6.0.3" />
<PackageReference Include="Huobi.Net" Version="5.0.3" />
<PackageReference Include="JK.Bitget.Net" Version="1.0.0" />
<PackageReference Include="JK.OKX.Net" Version="1.4.2" />
<PackageReference Include="KrakenExchange.Net" Version="4.1.5" />
<PackageReference Include="Kucoin.Net" Version="5.0.5" />
</ItemGroup>
</Project>
@@ -7,7 +7,6 @@ using Binance.Net.Clients;
using Binance.Net.Interfaces.Clients;
using ConsoleClient.Models;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
namespace ConsoleClient.Exchanges
@@ -2,7 +2,6 @@
using Bybit.Net.Interfaces.Clients;
using ConsoleClient.Models;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using System;
using System.Collections.Generic;
@@ -1,6 +1,5 @@
using ConsoleClient.Models;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using System;
using System.Collections.Generic;
-1
View File
@@ -8,7 +8,6 @@ using Binance.Net.Objects;
using Bybit.Net.Clients;
using ConsoleClient.Exchanges;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
namespace ConsoleClient
+15 -45
View File
@@ -1,6 +1,6 @@
# CryptoExchange.Net
[![.NET](https://img.shields.io/github/actions/workflow/status/JKorf/CryptoExchange.Net/dotnet.yml?style=for-the-badge)](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) [![Nuget downloads](https://img.shields.io/nuget/dt/CryptoExchange.Net.svg?style=for-the-badge)](https://www.nuget.org/packages/CryptoExchange.Net) ![License](https://img.shields.io/github/license/JKorf/CryptoExchange.Net?style=for-the-badge)
[![.NET](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml/badge.svg)](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) [![Nuget downloads](https://img.shields.io/nuget/dt/CryptoExchange.Net.svg)](https://www.nuget.org/packages/CryptoExchange.Net)
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.
@@ -11,21 +11,19 @@ The following API's are directly supported. Note that there are 3rd party implem
|Exchange|Repository|Nuget|
|--|--|--|
|Binance|[JKorf/Binance.Net](https://github.com/JKorf/Binance.Net)|[![Nuget version](https://img.shields.io/nuget/v/Binance.net.svg?style=flat-square)](https://www.nuget.org/packages/Binance.Net)|
|BingX|[JKorf/BingX.Net](https://github.com/JKorf/BingX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.BingX.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.BingX.Net)|
|Bitfinex|[JKorf/Bitfinex.Net](https://github.com/JKorf/Bitfinex.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bitfinex.net.svg?style=flat-square)](https://www.nuget.org/packages/Bitfinex.Net)|
|Bitget|[JKorf/Bitget.Net](https://github.com/JKorf/Bitget.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Bitget.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Bitget.Net)|
|Bybit|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square)](https://www.nuget.org/packages/Bybit.Net)|
|CoinEx|[JKorf/CoinEx.Net](https://github.com/JKorf/CoinEx.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinEx.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinEx.Net)|
|CoinGecko|[JKorf/CoinGecko.Net](https://github.com/JKorf/CoinGecko.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinGecko.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinGecko.Net)|
|Huobi/HTX|[JKorf/Huobi.Net](https://github.com/JKorf/Huobi.Net)|[![Nuget version](https://img.shields.io/nuget/v/Huobi.net.svg?style=flat-square)](https://www.nuget.org/packages/Huobi.Net)|
|Kraken|[JKorf/Kraken.Net](https://github.com/JKorf/Kraken.Net)|[![Nuget version](https://img.shields.io/nuget/v/KrakenExchange.net.svg?style=flat-square)](https://www.nuget.org/packages/KrakenExchange.Net)|
|Kucoin|[JKorf/Kucoin.Net](https://github.com/JKorf/Kucoin.Net)|[![Nuget version](https://img.shields.io/nuget/v/Kucoin.net.svg?style=flat-square)](https://www.nuget.org/packages/Kucoin.Net)|
|Mexc|[JKorf/Mexc.Net](https://github.com/JKorf/Mexc.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Mexc.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Mexc.Net)|
|OKX|[JKorf/OKX.Net](https://github.com/JKorf/OKX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.OKX.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.OKX.Net)|
|Binance|[JKorf/Binance.Net](https://github.com/JKorf/Binance.Net)|[![Nuget version](https://img.shields.io/nuget/v/Binance.net.svg)](https://www.nuget.org/packages/Binance.Net)|
|Bitfinex|[JKorf/Bitfinex.Net](https://github.com/JKorf/Bitfinex.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bitfinex.net.svg)](https://www.nuget.org/packages/Bitfinex.Net)|
|Bitget|[JKorf/Bitget.Net](https://github.com/JKorf/Bitget.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bitget.net.svg)](https://www.nuget.org/packages/Bitget.Net)|
|Bybit|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bybit.net.svg)](https://www.nuget.org/packages/Bybit.Net)|
|CoinEx|[JKorf/CoinEx.Net](https://github.com/JKorf/CoinEx.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinEx.net.svg)](https://www.nuget.org/packages/CoinEx.Net)|
|CoinGecko|[JKorf/CoinGecko.Net](https://github.com/JKorf/CoinGecko.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinGecko.net.svg)](https://www.nuget.org/packages/CoinGecko.Net)|
|Huobi/HTX|[JKorf/Huobi.Net](https://github.com/JKorf/Huobi.Net)|[![Nuget version](https://img.shields.io/nuget/v/Huobi.net.svg)](https://www.nuget.org/packages/Huobi.Net)|
|Kraken|[JKorf/Kraken.Net](https://github.com/JKorf/Kraken.Net)|[![Nuget version](https://img.shields.io/nuget/v/KrakenExchange.net.svg)](https://www.nuget.org/packages/KrakenExchange.Net)|
|Kucoin|[JKorf/Kucoin.Net](https://github.com/JKorf/Kucoin.Net)|[![Nuget version](https://img.shields.io/nuget/v/Kucoin.net.svg)](https://www.nuget.org/packages/Kucoin.Net)|
|Mexc|[JKorf/Mexc.Net](https://github.com/JKorf/Mexc.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Mexc.net.svg)](https://www.nuget.org/packages/JK.Mexc.Net)|
|OKX|[JKorf/OKX.Net](https://github.com/JKorf/OKX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.OKX.net.svg)](https://www.nuget.org/packages/JK.OKX.Net)|
## Discord
[![Nuget version](https://img.shields.io/discord/847020490588422145?style=for-the-badge)](https://discord.gg/MSpeEtSY8t)
A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free to join for discussion and/or questions around the CryptoExchange.Net and implementation libraries.
## Support the project
@@ -34,42 +32,14 @@ I develop and maintain this package on my own for free in my spare time, any sup
### Donate
Make a one time donation in a crypto currency of your choice. If you prefer to donate a currency not listed here please contact me.
**Btc**: bc1q277a5n54s2l2mzlu778ef7lpkwhjhyvghuv8qf
**Eth**: 0xcb1b63aCF9fef2755eBf4a0506250074496Ad5b7
**USDT (TRX)** TKigKeJPXZYyMVDgMyXxMf17MWYia92Rjd
**Btc**: bc1qz0jv0my7fc60rxeupr23e75x95qmlq6489n8gh
**Eth**: 0x8E21C4d955975cB645589745ac0c46ECA8FAE504
### Sponsor
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
## Release notes
* Version 7.2.1 - 05 Apr 2024
* Improved websocket reconnect logic
* Simplified SystemTextJsonMessageAccessor value retrieval
* Fixed System.Text.Json BoolConverter value writing
* Version 7.2.0 - 24 Mar 2024
* Added ArrayParametersSerialization.JsonArray support
* Refactored to high-performance logging for hot paths
* Updated SymbolOrderBook to use LoggerFactory
* Performance improvements
* Small bug fixes
* Updated logging
* Version 7.1.0 - 16 Mar 2024
* Added initial System.Text.Json deserialization support
* Added support for setting MessageSendSizeLimit for websocket clients to limit message size
* Added Exchange name property to IRestClient and ISocketClient interface
* Abstracted out rest client deserialization so different (de)serializers can be used
* Cleaned up rest client response handling
* Continued update of websocket message handling
* Use ReadonlyMemory<byte> to represent message data to prevent copying data multiple times
* Switched back to non-async websocket message handling to remove tasks overhead
* Updated package dependencies to latest versions
* Updated unit test package dependencies and updated tests accordingly
* Moved some properties used by the RestApiClient from the BaseApiClient
* Fixed issue with multiple concurrent subscribe calls in socket client
* Version 7.0.0 - 24 Feb 2024
* Version 7.0.0-beta1 - 06 Feb 2024
* Full overhaul of Websocket message handling
* Abstracted out Newtonsoft.Json references in preparation of moving to System.Text.Json
* Updated SendPeriodic to operate on connection level instead of client level to prevent looping when there are no connections
-1
View File
@@ -1 +0,0 @@
Source for https://jkorf.github.io/CryptoExchange.Net
-159
View File
@@ -1,159 +0,0 @@
/*============================
COLOR Blue
==============================*/
::selection {
background: #007bff;
}
a, a:focus {
color: #007bff;
}
a:hover, a:active {
color: #006adb;
}
.primary-menu ul.navbar-nav > li:hover > a:not(.btn), .primary-menu ul.navbar-nav > li > a.active:not(.btn) {
color: #007bff;
}
.primary-menu ul.navbar-nav > li.dropdown .dropdown-menu li:hover > a:not(.btn) {
color: #007bff;
}
.primary-menu.navbar-line-under-text ul.navbar-nav > li > a:not(.btn):after {
border-color: #007bff;
}
/*=== Side Navigation ===*/
.idocs-navigation .nav .nav .nav-item .nav-link.active:after, .idocs-navigation.docs-navigation-dark .nav .nav .nav-item .nav-link.active:after {
border-color: #007bff;
}
/* Accordion & Toggle */
.accordion .card-header a:hover.collapsed {
color: #007bff !important;
}
.accordion:not(.accordion-alternate) .card-header a {
background-color: #007bff;
color: #fff;
}
/* Nav */
.nav:not(.nav-pills) .nav-item .nav-link.active, .nav:not(.nav-pills) .nav-item .nav-link:hover {
color: #007bff;
}
.nav-tabs .nav-item .nav-link.active {
color: #0c2f55;
}
.nav-tabs .nav-item .nav-link.active:after {
background-color: #007bff;
}
.nav-tabs .nav-item .nav-link:not(.active):hover {
color: #007bff;
}
.nav-tabs.flex-column .nav-item .nav-link.active {
color: #007bff;
}
.nav-pills .nav-link:not(.active):hover {
color: #007bff;
}
#footer .nav .nav-item .nav-link:focus {
color: #007bff;
}
#footer .nav .nav-link:hover {
color: #007bff;
}
#footer .footer-copyright .nav .nav-link:hover {
color: #007bff;
}
/* Back to Top */
#back-to-top:hover {
background-color: #007bff;
}
/* Extras */
.bg-primary, .badge-primary {
background-color: #007bff !important;
}
.text-primary, .btn-light, .btn-outline-light:hover, .btn-link, .btn-outline-light:not(:disabled):not(.disabled).active, .btn-outline-light:not(:disabled):not(.disabled):active {
color: #007bff !important;
}
.btn-link:hover {
color: #006adb !important;
}
.text-muted {
color: #8e9a9d !important;
}
.text-light {
color: #dee3e4 !important;
}
a.bg-primary:focus, a.bg-primary:hover, button.bg-primary:focus, button.bg-primary:hover {
background-color: #006adb !important;
}
.border-primary {
border-color: #007bff !important;
}
.btn-primary {
background-color: #007bff;
border-color: #007bff;
}
.btn-primary:hover {
background-color: #006adb;
border-color: #006adb;
}
.btn-primary:not(:disabled):not(.disabled).active, .btn-primary:not(:disabled):not(.disabled):active {
background-color: #006adb;
border-color: #006adb;
}
.btn-primary.focus, .btn-primary:focus {
background-color: #006adb;
border-color: #006adb;
}
.btn-outline-primary, .btn-outline-primary:not(:disabled):not(.disabled).active, .btn-outline-primary:not(:disabled):not(.disabled):active {
color: #007bff;
border-color: #007bff;
}
.btn-outline-primary:hover, .btn-outline-primary:not(:disabled):not(.disabled).active:hover, .btn-outline-primary:not(:disabled):not(.disabled):active:hover {
background-color: #007bff;
border-color: #007bff;
color: #fff;
}
.progress-bar,
.nav-pills .nav-link.active, .nav-pills .show > .nav-link, .dropdown-item.active, .dropdown-item:active {
background-color: #007bff;
}
.page-item.active .page-link,
.custom-radio .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label::before,
.custom-checkbox .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label:before {
background-color: #007bff;
border-color: #007bff;
}
.list-group-item.active {
background-color: #007bff;
border-color: #007bff;
}
.page-link {
color: #007bff;
}
.page-link:hover {
color: #006adb;
}
-159
View File
@@ -1,159 +0,0 @@
/*============================
COLOR Brown
==============================*/
::selection {
background: #795548;
}
a, a:focus {
color: #795548;
}
a:hover, a:active {
color: #63453b;
}
.primary-menu ul.navbar-nav > li:hover > a:not(.btn), .primary-menu ul.navbar-nav > li > a.active:not(.btn) {
color: #795548;
}
.primary-menu ul.navbar-nav > li.dropdown .dropdown-menu li:hover > a:not(.btn) {
color: #795548;
}
.primary-menu.navbar-line-under-text ul.navbar-nav > li > a:not(.btn):after {
border-color: #795548;
}
/*=== Side Navigation ===*/
.idocs-navigation .nav .nav .nav-item .nav-link.active:after, .idocs-navigation.docs-navigation-dark .nav .nav .nav-item .nav-link.active:after {
border-color: #795548;
}
/* Accordion & Toggle */
.accordion .card-header a:hover.collapsed {
color: #795548 !important;
}
.accordion:not(.accordion-alternate) .card-header a {
background-color: #795548;
color: #fff;
}
/* Nav */
.nav:not(.nav-pills) .nav-item .nav-link.active, .nav:not(.nav-pills) .nav-item .nav-link:hover {
color: #795548;
}
.nav-tabs .nav-item .nav-link.active {
color: #0c2f55;
}
.nav-tabs .nav-item .nav-link.active:after {
background-color: #795548;
}
.nav-tabs .nav-item .nav-link:not(.active):hover {
color: #795548;
}
.nav-tabs.flex-column .nav-item .nav-link.active {
color: #795548;
}
.nav-pills .nav-link:not(.active):hover {
color: #795548;
}
#footer .nav .nav-item .nav-link:focus {
color: #795548;
}
#footer .nav .nav-link:hover {
color: #795548;
}
#footer .footer-copyright .nav .nav-link:hover {
color: #795548;
}
/* Back to Top */
#back-to-top:hover {
background-color: #795548;
}
/* Extras */
.bg-primary, .badge-primary {
background-color: #795548 !important;
}
.text-primary, .btn-light, .btn-outline-light:hover, .btn-link, .btn-outline-light:not(:disabled):not(.disabled).active, .btn-outline-light:not(:disabled):not(.disabled):active {
color: #795548 !important;
}
.btn-link:hover {
color: #63453b !important;
}
.text-muted {
color: #8e9a9d !important;
}
.text-light {
color: #dee3e4 !important;
}
a.bg-primary:focus, a.bg-primary:hover, button.bg-primary:focus, button.bg-primary:hover {
background-color: #63453b !important;
}
.border-primary {
border-color: #795548 !important;
}
.btn-primary {
background-color: #795548;
border-color: #795548;
}
.btn-primary:hover {
background-color: #63453b;
border-color: #63453b;
}
.btn-primary:not(:disabled):not(.disabled).active, .btn-primary:not(:disabled):not(.disabled):active {
background-color: #63453b;
border-color: #63453b;
}
.btn-primary.focus, .btn-primary:focus {
background-color: #63453b;
border-color: #63453b;
}
.btn-outline-primary, .btn-outline-primary:not(:disabled):not(.disabled).active, .btn-outline-primary:not(:disabled):not(.disabled):active {
color: #795548;
border-color: #795548;
}
.btn-outline-primary:hover, .btn-outline-primary:not(:disabled):not(.disabled).active:hover, .btn-outline-primary:not(:disabled):not(.disabled):active:hover {
background-color: #795548;
border-color: #795548;
color: #fff;
}
.progress-bar,
.nav-pills .nav-link.active, .nav-pills .show > .nav-link, .dropdown-item.active, .dropdown-item:active {
background-color: #795548;
}
.page-item.active .page-link,
.custom-radio .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label::before,
.custom-checkbox .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label:before {
background-color: #795548;
border-color: #795548;
}
.list-group-item.active {
background-color: #795548;
border-color: #795548;
}
.page-link {
color: #795548;
}
.page-link:hover {
color: #63453b;
}
-159
View File
@@ -1,159 +0,0 @@
/*============================
COLOR Cyan
==============================*/
::selection {
background: #17a2b8;
}
a, a:focus {
color: #17a2b8;
}
a:hover, a:active {
color: #138698;
}
.primary-menu ul.navbar-nav > li:hover > a:not(.btn), .primary-menu ul.navbar-nav > li > a.active:not(.btn) {
color: #17a2b8;
}
.primary-menu ul.navbar-nav > li.dropdown .dropdown-menu li:hover > a:not(.btn) {
color: #17a2b8;
}
.primary-menu.navbar-line-under-text ul.navbar-nav > li > a:not(.btn):after {
border-color: #17a2b8;
}
/*=== Side Navigation ===*/
.idocs-navigation .nav .nav .nav-item .nav-link.active:after, .idocs-navigation.docs-navigation-dark .nav .nav .nav-item .nav-link.active:after {
border-color: #17a2b8;
}
/* Accordion & Toggle */
.accordion .card-header a:hover.collapsed {
color: #17a2b8 !important;
}
.accordion:not(.accordion-alternate) .card-header a {
background-color: #17a2b8;
color: #fff;
}
/* Nav */
.nav:not(.nav-pills) .nav-item .nav-link.active, .nav:not(.nav-pills) .nav-item .nav-link:hover {
color: #17a2b8;
}
.nav-tabs .nav-item .nav-link.active {
color: #0c2f55;
}
.nav-tabs .nav-item .nav-link.active:after {
background-color: #17a2b8;
}
.nav-tabs .nav-item .nav-link:not(.active):hover {
color: #17a2b8;
}
.nav-tabs.flex-column .nav-item .nav-link.active {
color: #17a2b8;
}
.nav-pills .nav-link:not(.active):hover {
color: #17a2b8;
}
#footer .nav .nav-item .nav-link:focus {
color: #17a2b8;
}
#footer .nav .nav-link:hover {
color: #17a2b8;
}
#footer .footer-copyright .nav .nav-link:hover {
color: #17a2b8;
}
/* Back to Top */
#back-to-top:hover {
background-color: #17a2b8;
}
/* Extras */
.bg-primary, .badge-primary {
background-color: #17a2b8 !important;
}
.text-primary, .btn-light, .btn-outline-light:hover, .btn-link, .btn-outline-light:not(:disabled):not(.disabled).active, .btn-outline-light:not(:disabled):not(.disabled):active {
color: #17a2b8 !important;
}
.btn-link:hover {
color: #138698 !important;
}
.text-muted {
color: #8e9a9d !important;
}
.text-light {
color: #dee3e4 !important;
}
a.bg-primary:focus, a.bg-primary:hover, button.bg-primary:focus, button.bg-primary:hover {
background-color: #138698 !important;
}
.border-primary {
border-color: #17a2b8 !important;
}
.btn-primary {
background-color: #17a2b8;
border-color: #17a2b8;
}
.btn-primary:hover {
background-color: #138698;
border-color: #138698;
}
.btn-primary:not(:disabled):not(.disabled).active, .btn-primary:not(:disabled):not(.disabled):active {
background-color: #138698;
border-color: #138698;
}
.btn-primary.focus, .btn-primary:focus {
background-color: #138698;
border-color: #138698;
}
.btn-outline-primary, .btn-outline-primary:not(:disabled):not(.disabled).active, .btn-outline-primary:not(:disabled):not(.disabled):active {
color: #17a2b8;
border-color: #17a2b8;
}
.btn-outline-primary:hover, .btn-outline-primary:not(:disabled):not(.disabled).active:hover, .btn-outline-primary:not(:disabled):not(.disabled):active:hover {
background-color: #17a2b8;
border-color: #17a2b8;
color: #fff;
}
.progress-bar,
.nav-pills .nav-link.active, .nav-pills .show > .nav-link, .dropdown-item.active, .dropdown-item:active {
background-color: #17a2b8;
}
.page-item.active .page-link,
.custom-radio .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label::before,
.custom-checkbox .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label:before {
background-color: #17a2b8;
border-color: #17a2b8;
}
.list-group-item.active {
background-color: #17a2b8;
border-color: #17a2b8;
}
.page-link {
color: #17a2b8;
}
.page-link:hover {
color: #138698;
}
-159
View File
@@ -1,159 +0,0 @@
/*============================
COLOR Green
==============================*/
::selection {
background: #28a745;
}
a, a:focus {
color: #28a745;
}
a:hover, a:active {
color: #218a39;
}
.primary-menu ul.navbar-nav > li:hover > a:not(.btn), .primary-menu ul.navbar-nav > li > a.active:not(.btn) {
color: #28a745;
}
.primary-menu ul.navbar-nav > li.dropdown .dropdown-menu li:hover > a:not(.btn) {
color: #28a745;
}
.primary-menu.navbar-line-under-text ul.navbar-nav > li > a:not(.btn):after {
border-color: #28a745;
}
/*=== Side Navigation ===*/
.idocs-navigation .nav .nav .nav-item .nav-link.active:after, .idocs-navigation.docs-navigation-dark .nav .nav .nav-item .nav-link.active:after {
border-color: #28a745;
}
/* Accordion & Toggle */
.accordion .card-header a:hover.collapsed {
color: #28a745 !important;
}
.accordion:not(.accordion-alternate) .card-header a {
background-color: #28a745;
color: #fff;
}
/* Nav */
.nav:not(.nav-pills) .nav-item .nav-link.active, .nav:not(.nav-pills) .nav-item .nav-link:hover {
color: #28a745;
}
.nav-tabs .nav-item .nav-link.active {
color: #0c2f55;
}
.nav-tabs .nav-item .nav-link.active:after {
background-color: #28a745;
}
.nav-tabs .nav-item .nav-link:not(.active):hover {
color: #28a745;
}
.nav-tabs.flex-column .nav-item .nav-link.active {
color: #28a745;
}
.nav-pills .nav-link:not(.active):hover {
color: #28a745;
}
#footer .nav .nav-item .nav-link:focus {
color: #28a745;
}
#footer .nav .nav-link:hover {
color: #28a745;
}
#footer .footer-copyright .nav .nav-link:hover {
color: #28a745;
}
/* Back to Top */
#back-to-top:hover {
background-color: #28a745;
}
/* Extras */
.bg-primary, .badge-primary {
background-color: #28a745 !important;
}
.text-primary, .btn-light, .btn-outline-light:hover, .btn-link, .btn-outline-light:not(:disabled):not(.disabled).active, .btn-outline-light:not(:disabled):not(.disabled):active {
color: #28a745 !important;
}
.btn-link:hover {
color: #218a39 !important;
}
.text-muted {
color: #8e9a9d !important;
}
.text-light {
color: #dee3e4 !important;
}
a.bg-primary:focus, a.bg-primary:hover, button.bg-primary:focus, button.bg-primary:hover {
background-color: #218a39 !important;
}
.border-primary {
border-color: #28a745 !important;
}
.btn-primary {
background-color: #28a745;
border-color: #28a745;
}
.btn-primary:hover {
background-color: #218a39;
border-color: #218a39;
}
.btn-primary:not(:disabled):not(.disabled).active, .btn-primary:not(:disabled):not(.disabled):active {
background-color: #218a39;
border-color: #218a39;
}
.btn-primary.focus, .btn-primary:focus {
background-color: #218a39;
border-color: #218a39;
}
.btn-outline-primary, .btn-outline-primary:not(:disabled):not(.disabled).active, .btn-outline-primary:not(:disabled):not(.disabled):active {
color: #28a745;
border-color: #28a745;
}
.btn-outline-primary:hover, .btn-outline-primary:not(:disabled):not(.disabled).active:hover, .btn-outline-primary:not(:disabled):not(.disabled):active:hover {
background-color: #28a745;
border-color: #28a745;
color: #fff;
}
.progress-bar,
.nav-pills .nav-link.active, .nav-pills .show > .nav-link, .dropdown-item.active, .dropdown-item:active {
background-color: #28a745;
}
.page-item.active .page-link,
.custom-radio .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label::before,
.custom-checkbox .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label:before {
background-color: #28a745;
border-color: #28a745;
}
.list-group-item.active {
background-color: #28a745;
border-color: #28a745;
}
.page-link {
color: #28a745;
}
.page-link:hover {
color: #218a39;
}
-159
View File
@@ -1,159 +0,0 @@
/*============================
COLOR Indigo
==============================*/
::selection {
background: #6610f2;
}
a, a:focus {
color: #6610f2;
}
a:hover, a:active {
color: #570bd3;
}
.primary-menu ul.navbar-nav > li:hover > a:not(.btn), .primary-menu ul.navbar-nav > li > a.active:not(.btn) {
color: #6610f2;
}
.primary-menu ul.navbar-nav > li.dropdown .dropdown-menu li:hover > a:not(.btn) {
color: #6610f2;
}
.primary-menu.navbar-line-under-text ul.navbar-nav > li > a:not(.btn):after {
border-color: #6610f2;
}
/*=== Side Navigation ===*/
.idocs-navigation .nav .nav .nav-item .nav-link.active:after, .idocs-navigation.docs-navigation-dark .nav .nav .nav-item .nav-link.active:after {
border-color: #6610f2;
}
/* Accordion & Toggle */
.accordion .card-header a:hover.collapsed {
color: #6610f2 !important;
}
.accordion:not(.accordion-alternate) .card-header a {
background-color: #6610f2;
color: #fff;
}
/* Nav */
.nav:not(.nav-pills) .nav-item .nav-link.active, .nav:not(.nav-pills) .nav-item .nav-link:hover {
color: #6610f2;
}
.nav-tabs .nav-item .nav-link.active {
color: #0c2f55;
}
.nav-tabs .nav-item .nav-link.active:after {
background-color: #6610f2;
}
.nav-tabs .nav-item .nav-link:not(.active):hover {
color: #6610f2;
}
.nav-tabs.flex-column .nav-item .nav-link.active {
color: #6610f2;
}
.nav-pills .nav-link:not(.active):hover {
color: #6610f2;
}
#footer .nav .nav-item .nav-link:focus {
color: #6610f2;
}
#footer .nav .nav-link:hover {
color: #6610f2;
}
#footer .footer-copyright .nav .nav-link:hover {
color: #6610f2;
}
/* Back to Top */
#back-to-top:hover {
background-color: #6610f2;
}
/* Extras */
.bg-primary, .badge-primary {
background-color: #6610f2 !important;
}
.text-primary, .btn-light, .btn-outline-light:hover, .btn-link, .btn-outline-light:not(:disabled):not(.disabled).active, .btn-outline-light:not(:disabled):not(.disabled):active {
color: #6610f2 !important;
}
.btn-link:hover {
color: #570bd3 !important;
}
.text-muted {
color: #8e9a9d !important;
}
.text-light {
color: #dee3e4 !important;
}
a.bg-primary:focus, a.bg-primary:hover, button.bg-primary:focus, button.bg-primary:hover {
background-color: #570bd3 !important;
}
.border-primary {
border-color: #6610f2 !important;
}
.btn-primary {
background-color: #6610f2;
border-color: #6610f2;
}
.btn-primary:hover {
background-color: #570bd3;
border-color: #570bd3;
}
.btn-primary:not(:disabled):not(.disabled).active, .btn-primary:not(:disabled):not(.disabled):active {
background-color: #570bd3;
border-color: #570bd3;
}
.btn-primary.focus, .btn-primary:focus {
background-color: #570bd3;
border-color: #570bd3;
}
.btn-outline-primary, .btn-outline-primary:not(:disabled):not(.disabled).active, .btn-outline-primary:not(:disabled):not(.disabled):active {
color: #6610f2;
border-color: #6610f2;
}
.btn-outline-primary:hover, .btn-outline-primary:not(:disabled):not(.disabled).active:hover, .btn-outline-primary:not(:disabled):not(.disabled):active:hover {
background-color: #6610f2;
border-color: #6610f2;
color: #fff;
}
.progress-bar,
.nav-pills .nav-link.active, .nav-pills .show > .nav-link, .dropdown-item.active, .dropdown-item:active {
background-color: #6610f2;
}
.page-item.active .page-link,
.custom-radio .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label::before,
.custom-checkbox .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label:before {
background-color: #6610f2;
border-color: #6610f2;
}
.list-group-item.active {
background-color: #6610f2;
border-color: #6610f2;
}
.page-link {
color: #6610f2;
}
.page-link:hover {
color: #570bd3;
}
-159
View File
@@ -1,159 +0,0 @@
/*============================
COLOR Orange
==============================*/
::selection {
background: #fd7e14;
}
a, a:focus {
color: #fd7e14;
}
a:hover, a:active {
color: #eb6c02;
}
.primary-menu ul.navbar-nav > li:hover > a:not(.btn), .primary-menu ul.navbar-nav > li > a.active:not(.btn) {
color: #fd7e14;
}
.primary-menu ul.navbar-nav > li.dropdown .dropdown-menu li:hover > a:not(.btn) {
color: #fd7e14;
}
.primary-menu.navbar-line-under-text ul.navbar-nav > li > a:not(.btn):after {
border-color: #fd7e14;
}
/*=== Side Navigation ===*/
.idocs-navigation .nav .nav .nav-item .nav-link.active:after, .idocs-navigation.docs-navigation-dark .nav .nav .nav-item .nav-link.active:after {
border-color: #fd7e14;
}
/* Accordion & Toggle */
.accordion .card-header a:hover.collapsed {
color: #fd7e14 !important;
}
.accordion:not(.accordion-alternate) .card-header a {
background-color: #fd7e14;
color: #fff;
}
/* Nav */
.nav:not(.nav-pills) .nav-item .nav-link.active, .nav:not(.nav-pills) .nav-item .nav-link:hover {
color: #fd7e14;
}
.nav-tabs .nav-item .nav-link.active {
color: #0c2f55;
}
.nav-tabs .nav-item .nav-link.active:after {
background-color: #fd7e14;
}
.nav-tabs .nav-item .nav-link:not(.active):hover {
color: #fd7e14;
}
.nav-tabs.flex-column .nav-item .nav-link.active {
color: #fd7e14;
}
.nav-pills .nav-link:not(.active):hover {
color: #fd7e14;
}
#footer .nav .nav-item .nav-link:focus {
color: #fd7e14;
}
#footer .nav .nav-link:hover {
color: #fd7e14;
}
#footer .footer-copyright .nav .nav-link:hover {
color: #fd7e14;
}
/* Back to Top */
#back-to-top:hover {
background-color: #fd7e14;
}
/* Extras */
.bg-primary, .badge-primary {
background-color: #fd7e14 !important;
}
.text-primary, .btn-light, .btn-outline-light:hover, .btn-link, .btn-outline-light:not(:disabled):not(.disabled).active, .btn-outline-light:not(:disabled):not(.disabled):active {
color: #fd7e14 !important;
}
.btn-link:hover {
color: #eb6c02 !important;
}
.text-muted {
color: #8e9a9d !important;
}
.text-light {
color: #dee3e4 !important;
}
a.bg-primary:focus, a.bg-primary:hover, button.bg-primary:focus, button.bg-primary:hover {
background-color: #eb6c02 !important;
}
.border-primary {
border-color: #fd7e14 !important;
}
.btn-primary {
background-color: #fd7e14;
border-color: #fd7e14;
}
.btn-primary:hover {
background-color: #eb6c02;
border-color: #eb6c02;
}
.btn-primary:not(:disabled):not(.disabled).active, .btn-primary:not(:disabled):not(.disabled):active {
background-color: #eb6c02;
border-color: #eb6c02;
}
.btn-primary.focus, .btn-primary:focus {
background-color: #eb6c02;
border-color: #eb6c02;
}
.btn-outline-primary, .btn-outline-primary:not(:disabled):not(.disabled).active, .btn-outline-primary:not(:disabled):not(.disabled):active {
color: #fd7e14;
border-color: #fd7e14;
}
.btn-outline-primary:hover, .btn-outline-primary:not(:disabled):not(.disabled).active:hover, .btn-outline-primary:not(:disabled):not(.disabled):active:hover {
background-color: #fd7e14;
border-color: #fd7e14;
color: #fff;
}
.progress-bar,
.nav-pills .nav-link.active, .nav-pills .show > .nav-link, .dropdown-item.active, .dropdown-item:active {
background-color: #fd7e14;
}
.page-item.active .page-link,
.custom-radio .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label::before,
.custom-checkbox .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label:before {
background-color: #fd7e14;
border-color: #fd7e14;
}
.list-group-item.active {
background-color: #fd7e14;
border-color: #fd7e14;
}
.page-link {
color: #fd7e14;
}
.page-link:hover {
color: #eb6c02;
}
-159
View File
@@ -1,159 +0,0 @@
/*============================
COLOR Purple
==============================*/
::selection {
background: #6f42c1;
}
a, a:focus {
color: #6f42c1;
}
a:hover, a:active {
color: #5f37a8;
}
.primary-menu ul.navbar-nav > li:hover > a:not(.btn), .primary-menu ul.navbar-nav > li > a.active:not(.btn) {
color: #6f42c1;
}
.primary-menu ul.navbar-nav > li.dropdown .dropdown-menu li:hover > a:not(.btn) {
color: #6f42c1;
}
.primary-menu.navbar-line-under-text ul.navbar-nav > li > a:not(.btn):after {
border-color: #6f42c1;
}
/*=== Side Navigation ===*/
.idocs-navigation .nav .nav .nav-item .nav-link.active:after, .idocs-navigation.docs-navigation-dark .nav .nav .nav-item .nav-link.active:after {
border-color: #6f42c1;
}
/* Accordion & Toggle */
.accordion .card-header a:hover.collapsed {
color: #6f42c1 !important;
}
.accordion:not(.accordion-alternate) .card-header a {
background-color: #6f42c1;
color: #fff;
}
/* Nav */
.nav:not(.nav-pills) .nav-item .nav-link.active, .nav:not(.nav-pills) .nav-item .nav-link:hover {
color: #6f42c1;
}
.nav-tabs .nav-item .nav-link.active {
color: #0c2f55;
}
.nav-tabs .nav-item .nav-link.active:after {
background-color: #6f42c1;
}
.nav-tabs .nav-item .nav-link:not(.active):hover {
color: #6f42c1;
}
.nav-tabs.flex-column .nav-item .nav-link.active {
color: #6f42c1;
}
.nav-pills .nav-link:not(.active):hover {
color: #6f42c1;
}
#footer .nav .nav-item .nav-link:focus {
color: #6f42c1;
}
#footer .nav .nav-link:hover {
color: #6f42c1;
}
#footer .footer-copyright .nav .nav-link:hover {
color: #6f42c1;
}
/* Back to Top */
#back-to-top:hover {
background-color: #6f42c1;
}
/* Extras */
.bg-primary, .badge-primary {
background-color: #6f42c1 !important;
}
.text-primary, .btn-light, .btn-outline-light:hover, .btn-link, .btn-outline-light:not(:disabled):not(.disabled).active, .btn-outline-light:not(:disabled):not(.disabled):active {
color: #6f42c1 !important;
}
.btn-link:hover {
color: #5f37a8 !important;
}
.text-muted {
color: #8e9a9d !important;
}
.text-light {
color: #dee3e4 !important;
}
a.bg-primary:focus, a.bg-primary:hover, button.bg-primary:focus, button.bg-primary:hover {
background-color: #5f37a8 !important;
}
.border-primary {
border-color: #6f42c1 !important;
}
.btn-primary {
background-color: #6f42c1;
border-color: #6f42c1;
}
.btn-primary:hover {
background-color: #5f37a8;
border-color: #5f37a8;
}
.btn-primary:not(:disabled):not(.disabled).active, .btn-primary:not(:disabled):not(.disabled):active {
background-color: #5f37a8;
border-color: #5f37a8;
}
.btn-primary.focus, .btn-primary:focus {
background-color: #5f37a8;
border-color: #5f37a8;
}
.btn-outline-primary, .btn-outline-primary:not(:disabled):not(.disabled).active, .btn-outline-primary:not(:disabled):not(.disabled):active {
color: #6f42c1;
border-color: #6f42c1;
}
.btn-outline-primary:hover, .btn-outline-primary:not(:disabled):not(.disabled).active:hover, .btn-outline-primary:not(:disabled):not(.disabled):active:hover {
background-color: #6f42c1;
border-color: #6f42c1;
color: #fff;
}
.progress-bar,
.nav-pills .nav-link.active, .nav-pills .show > .nav-link, .dropdown-item.active, .dropdown-item:active {
background-color: #6f42c1;
}
.page-item.active .page-link,
.custom-radio .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label::before,
.custom-checkbox .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label:before {
background-color: #6f42c1;
border-color: #6f42c1;
}
.list-group-item.active {
background-color: #6f42c1;
border-color: #6f42c1;
}
.page-link {
color: #6f42c1;
}
.page-link:hover {
color: #5f37a8;
}
-159
View File
@@ -1,159 +0,0 @@
/*============================
COLOR Red
==============================*/
::selection {
background: #dc3545;
}
a, a:focus {
color: #dc3545;
}
a:hover, a:active {
color: #ca2333;
}
.primary-menu ul.navbar-nav > li:hover > a:not(.btn), .primary-menu ul.navbar-nav > li > a.active:not(.btn) {
color: #dc3545;
}
.primary-menu ul.navbar-nav > li.dropdown .dropdown-menu li:hover > a:not(.btn) {
color: #dc3545;
}
.primary-menu.navbar-line-under-text ul.navbar-nav > li > a:not(.btn):after {
border-color: #dc3545;
}
/*=== Side Navigation ===*/
.idocs-navigation .nav .nav .nav-item .nav-link.active:after, .idocs-navigation.docs-navigation-dark .nav .nav .nav-item .nav-link.active:after {
border-color: #dc3545;
}
/* Accordion & Toggle */
.accordion .card-header a:hover.collapsed {
color: #dc3545 !important;
}
.accordion:not(.accordion-alternate) .card-header a {
background-color: #dc3545;
color: #fff;
}
/* Nav */
.nav:not(.nav-pills) .nav-item .nav-link.active, .nav:not(.nav-pills) .nav-item .nav-link:hover {
color: #dc3545;
}
.nav-tabs .nav-item .nav-link.active {
color: #0c2f55;
}
.nav-tabs .nav-item .nav-link.active:after {
background-color: #dc3545;
}
.nav-tabs .nav-item .nav-link:not(.active):hover {
color: #dc3545;
}
.nav-tabs.flex-column .nav-item .nav-link.active {
color: #dc3545;
}
.nav-pills .nav-link:not(.active):hover {
color: #dc3545;
}
#footer .nav .nav-item .nav-link:focus {
color: #dc3545;
}
#footer .nav .nav-link:hover {
color: #dc3545;
}
#footer .footer-copyright .nav .nav-link:hover {
color: #dc3545;
}
/* Back to Top */
#back-to-top:hover {
background-color: #dc3545;
}
/* Extras */
.bg-primary, .badge-primary {
background-color: #dc3545 !important;
}
.text-primary, .btn-light, .btn-outline-light:hover, .btn-link, .btn-outline-light:not(:disabled):not(.disabled).active, .btn-outline-light:not(:disabled):not(.disabled):active {
color: #dc3545 !important;
}
.btn-link:hover {
color: #ca2333 !important;
}
.text-muted {
color: #8e9a9d !important;
}
.text-light {
color: #dee3e4 !important;
}
a.bg-primary:focus, a.bg-primary:hover, button.bg-primary:focus, button.bg-primary:hover {
background-color: #ca2333 !important;
}
.border-primary {
border-color: #dc3545 !important;
}
.btn-primary {
background-color: #dc3545;
border-color: #dc3545;
}
.btn-primary:hover {
background-color: #ca2333;
border-color: #ca2333;
}
.btn-primary:not(:disabled):not(.disabled).active, .btn-primary:not(:disabled):not(.disabled):active {
background-color: #ca2333;
border-color: #ca2333;
}
.btn-primary.focus, .btn-primary:focus {
background-color: #ca2333;
border-color: #ca2333;
}
.btn-outline-primary, .btn-outline-primary:not(:disabled):not(.disabled).active, .btn-outline-primary:not(:disabled):not(.disabled):active {
color: #dc3545;
border-color: #dc3545;
}
.btn-outline-primary:hover, .btn-outline-primary:not(:disabled):not(.disabled).active:hover, .btn-outline-primary:not(:disabled):not(.disabled):active:hover {
background-color: #dc3545;
border-color: #dc3545;
color: #fff;
}
.progress-bar,
.nav-pills .nav-link.active, .nav-pills .show > .nav-link, .dropdown-item.active, .dropdown-item:active {
background-color: #dc3545;
}
.page-item.active .page-link,
.custom-radio .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label::before,
.custom-checkbox .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label:before {
background-color: #dc3545;
border-color: #dc3545;
}
.list-group-item.active {
background-color: #dc3545;
border-color: #dc3545;
}
.page-link {
color: #dc3545;
}
.page-link:hover {
color: #ca2333;
}
-159
View File
@@ -1,159 +0,0 @@
/*============================
COLOR Teal
==============================*/
::selection {
background: #20c997;
}
a, a:focus {
color: #20c997;
}
a:hover, a:active {
color: #1baa80;
}
.primary-menu ul.navbar-nav > li:hover > a:not(.btn), .primary-menu ul.navbar-nav > li > a.active:not(.btn) {
color: #20c997;
}
.primary-menu ul.navbar-nav > li.dropdown .dropdown-menu li:hover > a:not(.btn) {
color: #20c997;
}
.primary-menu.navbar-line-under-text ul.navbar-nav > li > a:not(.btn):after {
border-color: #20c997;
}
/*=== Side Navigation ===*/
.idocs-navigation .nav .nav .nav-item .nav-link.active:after, .idocs-navigation.docs-navigation-dark .nav .nav .nav-item .nav-link.active:after {
border-color: #20c997;
}
/* Accordion & Toggle */
.accordion .card-header a:hover.collapsed {
color: #20c997 !important;
}
.accordion:not(.accordion-alternate) .card-header a {
background-color: #20c997;
color: #fff;
}
/* Nav */
.nav:not(.nav-pills) .nav-item .nav-link.active, .nav:not(.nav-pills) .nav-item .nav-link:hover {
color: #20c997;
}
.nav-tabs .nav-item .nav-link.active {
color: #0c2f55;
}
.nav-tabs .nav-item .nav-link.active:after {
background-color: #20c997;
}
.nav-tabs .nav-item .nav-link:not(.active):hover {
color: #20c997;
}
.nav-tabs.flex-column .nav-item .nav-link.active {
color: #20c997;
}
.nav-pills .nav-link:not(.active):hover {
color: #20c997;
}
#footer .nav .nav-item .nav-link:focus {
color: #20c997;
}
#footer .nav .nav-link:hover {
color: #20c997;
}
#footer .footer-copyright .nav .nav-link:hover {
color: #20c997;
}
/* Back to Top */
#back-to-top:hover {
background-color: #20c997;
}
/* Extras */
.bg-primary, .badge-primary {
background-color: #20c997 !important;
}
.text-primary, .btn-light, .btn-outline-light:hover, .btn-link, .btn-outline-light:not(:disabled):not(.disabled).active, .btn-outline-light:not(:disabled):not(.disabled):active {
color: #20c997 !important;
}
.btn-link:hover {
color: #1baa80 !important;
}
.text-muted {
color: #8e9a9d !important;
}
.text-light {
color: #dee3e4 !important;
}
a.bg-primary:focus, a.bg-primary:hover, button.bg-primary:focus, button.bg-primary:hover {
background-color: #1baa80 !important;
}
.border-primary {
border-color: #20c997 !important;
}
.btn-primary {
background-color: #20c997;
border-color: #20c997;
}
.btn-primary:hover {
background-color: #1baa80;
border-color: #1baa80;
}
.btn-primary:not(:disabled):not(.disabled).active, .btn-primary:not(:disabled):not(.disabled):active {
background-color: #1baa80;
border-color: #1baa80;
}
.btn-primary.focus, .btn-primary:focus {
background-color: #1baa80;
border-color: #1baa80;
}
.btn-outline-primary, .btn-outline-primary:not(:disabled):not(.disabled).active, .btn-outline-primary:not(:disabled):not(.disabled):active {
color: #20c997;
border-color: #20c997;
}
.btn-outline-primary:hover, .btn-outline-primary:not(:disabled):not(.disabled).active:hover, .btn-outline-primary:not(:disabled):not(.disabled):active:hover {
background-color: #20c997;
border-color: #20c997;
color: #fff;
}
.progress-bar,
.nav-pills .nav-link.active, .nav-pills .show > .nav-link, .dropdown-item.active, .dropdown-item:active {
background-color: #20c997;
}
.page-item.active .page-link,
.custom-radio .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label::before,
.custom-checkbox .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label:before {
background-color: #20c997;
border-color: #20c997;
}
.list-group-item.active {
background-color: #20c997;
border-color: #20c997;
}
.page-link {
color: #20c997;
}
.page-link:hover {
color: #1baa80;
}

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