mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 17:03:10 +00:00
Compare commits
49 Commits
7.0.0-beta1
..
7.3.3
| Author | SHA1 | Date | |
|---|---|---|---|
| bb7ba5ea49 | |||
| 747c986644 | |||
| d88087c8ac | |||
| 968bdc330e | |||
| 7b49562c1d | |||
| 24ba60da47 | |||
| ed5a07fbdb | |||
| 3d3a9b88e7 | |||
| b2f9d5753e | |||
| de46c7bd1d | |||
| 5b11d94f73 | |||
| 6f915a3739 | |||
| d5c4b1bd01 | |||
| 1b1961db00 | |||
| 2dbd5be924 | |||
| 24c40d2dc6 | |||
| 5ef6feb996 | |||
| 85dad6f6f0 | |||
| 3cdcf0d9be | |||
| b90a0a71e9 | |||
| e62786a70f | |||
| 87722f2d28 | |||
| a86276f18d | |||
| f432a66016 | |||
| 9e2910d2ec | |||
| 46fbc1eb85 | |||
| f397d3ab94 | |||
| 81a2da1f3f | |||
| af3303c7b8 | |||
| 8ddd9ecf22 | |||
| de72fe4fb9 | |||
| 108c8fc183 | |||
| e86713e949 | |||
| db9fba4cf2 | |||
| 926802d953 | |||
| 87f5e12b60 | |||
| 034eb83bae | |||
| 7f29275851 | |||
| 2fb3442800 | |||
| 462c857bba | |||
| 61aa589cda | |||
| 27704bf090 | |||
| 4c899861b1 | |||
| 0cff678c2d | |||
| d18514d73c | |||
| 84736cac3f | |||
| cda1cce495 | |||
| fe3a0afd6c | |||
| d533557324 |
@@ -287,5 +287,3 @@ __pycache__/
|
|||||||
*.odx.cs
|
*.odx.cs
|
||||||
*.xsd.cs
|
*.xsd.cs
|
||||||
CryptoExchange.Net/CryptoExchange.Net.xml
|
CryptoExchange.Net/CryptoExchange.Net.xml
|
||||||
/Docs/*
|
|
||||||
Docs/
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NUnit.Framework.Legacy;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -24,8 +25,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var result1 = await waiter1;
|
var result1 = await waiter1;
|
||||||
var result2 = await waiter2;
|
var result2 = await waiter2;
|
||||||
|
|
||||||
Assert.True(result1);
|
Assert.That(result1);
|
||||||
Assert.True(result2);
|
Assert.That(result2);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -39,8 +40,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var result1 = await waiter1;
|
var result1 = await waiter1;
|
||||||
var result2 = await waiter2;
|
var result2 = await waiter2;
|
||||||
|
|
||||||
Assert.True(result1);
|
Assert.That(result1);
|
||||||
Assert.True(result2);
|
Assert.That(result2);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -55,14 +56,14 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
var result1 = await waiter1;
|
var result1 = await waiter1;
|
||||||
|
|
||||||
Assert.True(result1);
|
Assert.That(result1);
|
||||||
Assert.True(waiter2.Status != TaskStatus.RanToCompletion);
|
Assert.That(waiter2.Status != TaskStatus.RanToCompletion);
|
||||||
|
|
||||||
evnt.Set();
|
evnt.Set();
|
||||||
|
|
||||||
var result2 = await waiter2;
|
var result2 = await waiter2;
|
||||||
|
|
||||||
Assert.True(result2);
|
Assert.That(result2);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -75,13 +76,13 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
var result1 = await waiter1;
|
var result1 = await waiter1;
|
||||||
|
|
||||||
Assert.True(result1);
|
Assert.That(result1);
|
||||||
Assert.True(waiter2.Status != TaskStatus.RanToCompletion);
|
Assert.That(waiter2.Status != TaskStatus.RanToCompletion);
|
||||||
evnt.Set();
|
evnt.Set();
|
||||||
|
|
||||||
var result2 = await waiter2;
|
var result2 = await waiter2;
|
||||||
|
|
||||||
Assert.True(result2);
|
Assert.That(result2);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -105,12 +106,12 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
for(var i = 1; i <= 10; i++)
|
for(var i = 1; i <= 10; i++)
|
||||||
{
|
{
|
||||||
evnt.Set();
|
evnt.Set();
|
||||||
Assert.AreEqual(10 - i, waiters.Count(w => w.Status != TaskStatus.RanToCompletion));
|
Assert.That(10 - i == waiters.Count(w => w.Status != TaskStatus.RanToCompletion));
|
||||||
}
|
}
|
||||||
|
|
||||||
await resultsWaiter;
|
await resultsWaiter;
|
||||||
|
|
||||||
Assert.AreEqual(10, results.Count(r => r));
|
Assert.That(10 == results.Count(r => r));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -124,7 +125,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
var result1 = await waiter1;
|
var result1 = await waiter1;
|
||||||
|
|
||||||
Assert.True(result1);
|
Assert.That(result1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -134,9 +135,9 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
var waiter1 = evnt.WaitAsync(TimeSpan.FromMilliseconds(100));
|
var waiter1 = evnt.WaitAsync(TimeSpan.FromMilliseconds(100));
|
||||||
|
|
||||||
var result1 = await waiter1;
|
var result1 = await waiter1;
|
||||||
|
|
||||||
Assert.False(result1);
|
ClassicAssert.False(result1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using CryptoExchange.Net.Objects;
|
|||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NUnit.Framework.Legacy;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123}");
|
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123}");
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.IsTrue(result.Success);
|
Assert.That(result.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
@@ -34,8 +35,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123");
|
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123");
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.IsFalse(result.Success);
|
ClassicAssert.IsFalse(result.Success);
|
||||||
Assert.IsTrue(result.Error != null);
|
Assert.That(result.Error != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("https://api.test.com/api", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")]
|
[TestCase("https://api.test.com/api", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")]
|
||||||
@@ -48,7 +49,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void AppendPathTests(string baseUrl, string[] path, string expected)
|
public void AppendPathTests(string baseUrl, string[] path, string expected)
|
||||||
{
|
{
|
||||||
var result = baseUrl.AppendPath(path);
|
var result = baseUrl.AppendPath(path);
|
||||||
Assert.AreEqual(expected, result);
|
Assert.That(expected == result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NUnit.Framework.Legacy;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -17,9 +18,9 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var result = new CallResult(new ServerError("TestError"));
|
var result = new CallResult(new ServerError("TestError"));
|
||||||
|
|
||||||
Assert.AreEqual(result.Error.Message, "TestError");
|
ClassicAssert.AreSame(result.Error.Message, "TestError");
|
||||||
Assert.IsFalse(result);
|
ClassicAssert.IsFalse(result);
|
||||||
Assert.IsFalse(result.Success);
|
ClassicAssert.IsFalse(result.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -27,9 +28,9 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var result = new CallResult(null);
|
var result = new CallResult(null);
|
||||||
|
|
||||||
Assert.IsNull(result.Error);
|
ClassicAssert.IsNull(result.Error);
|
||||||
Assert.IsTrue(result);
|
Assert.That(result);
|
||||||
Assert.IsTrue(result.Success);
|
Assert.That(result.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -37,10 +38,10 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var result = new CallResult<object>(new ServerError("TestError"));
|
var result = new CallResult<object>(new ServerError("TestError"));
|
||||||
|
|
||||||
Assert.AreEqual(result.Error.Message, "TestError");
|
ClassicAssert.AreSame(result.Error.Message, "TestError");
|
||||||
Assert.IsNull(result.Data);
|
ClassicAssert.IsNull(result.Data);
|
||||||
Assert.IsFalse(result);
|
ClassicAssert.IsFalse(result);
|
||||||
Assert.IsFalse(result.Success);
|
ClassicAssert.IsFalse(result.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -48,10 +49,10 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var result = new CallResult<object>(new object());
|
var result = new CallResult<object>(new object());
|
||||||
|
|
||||||
Assert.IsNull(result.Error);
|
ClassicAssert.IsNull(result.Error);
|
||||||
Assert.IsNotNull(result.Data);
|
ClassicAssert.IsNotNull(result.Data);
|
||||||
Assert.IsTrue(result);
|
Assert.That(result);
|
||||||
Assert.IsTrue(result.Success);
|
Assert.That(result.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -60,11 +61,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var result = new CallResult<TestObjectResult>(new TestObjectResult());
|
var result = new CallResult<TestObjectResult>(new TestObjectResult());
|
||||||
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
||||||
|
|
||||||
Assert.IsNull(asResult.Error);
|
ClassicAssert.IsNull(asResult.Error);
|
||||||
Assert.IsNotNull(asResult.Data);
|
ClassicAssert.IsNotNull(asResult.Data);
|
||||||
Assert.IsTrue(asResult.Data is TestObject2);
|
Assert.That(asResult.Data is not null);
|
||||||
Assert.IsTrue(asResult);
|
Assert.That(asResult);
|
||||||
Assert.IsTrue(asResult.Success);
|
Assert.That(asResult.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -73,11 +74,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
|
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
|
||||||
var asResult = result.As<TestObject2>(default);
|
var asResult = result.As<TestObject2>(default);
|
||||||
|
|
||||||
Assert.IsNotNull(asResult.Error);
|
ClassicAssert.IsNotNull(asResult.Error);
|
||||||
Assert.AreEqual(asResult.Error.Message, "TestError");
|
ClassicAssert.AreSame(asResult.Error.Message, "TestError");
|
||||||
Assert.IsNull(asResult.Data);
|
ClassicAssert.IsNull(asResult.Data);
|
||||||
Assert.IsFalse(asResult);
|
ClassicAssert.IsFalse(asResult);
|
||||||
Assert.IsFalse(asResult.Success);
|
ClassicAssert.IsFalse(asResult.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -86,11 +87,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
|
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
|
||||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||||
|
|
||||||
Assert.IsNotNull(asResult.Error);
|
ClassicAssert.IsNotNull(asResult.Error);
|
||||||
Assert.AreEqual(asResult.Error.Message, "TestError2");
|
ClassicAssert.AreSame(asResult.Error.Message, "TestError2");
|
||||||
Assert.IsNull(asResult.Data);
|
ClassicAssert.IsNull(asResult.Data);
|
||||||
Assert.IsFalse(asResult);
|
ClassicAssert.IsFalse(asResult);
|
||||||
Assert.IsFalse(asResult.Success);
|
ClassicAssert.IsFalse(asResult.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -99,11 +100,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var result = new WebCallResult<TestObjectResult>(new ServerError("TestError"));
|
var result = new WebCallResult<TestObjectResult>(new ServerError("TestError"));
|
||||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||||
|
|
||||||
Assert.IsNotNull(asResult.Error);
|
ClassicAssert.IsNotNull(asResult.Error);
|
||||||
Assert.AreEqual(asResult.Error.Message, "TestError2");
|
ClassicAssert.AreSame(asResult.Error.Message, "TestError2");
|
||||||
Assert.IsNull(asResult.Data);
|
ClassicAssert.IsNull(asResult.Data);
|
||||||
Assert.IsFalse(asResult);
|
ClassicAssert.IsFalse(asResult);
|
||||||
Assert.IsFalse(asResult.Success);
|
ClassicAssert.IsFalse(asResult.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -124,15 +125,15 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
null);
|
null);
|
||||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||||
|
|
||||||
Assert.IsNotNull(asResult.Error);
|
ClassicAssert.IsNotNull(asResult.Error);
|
||||||
Assert.AreEqual(asResult.Error.Message, "TestError2");
|
Assert.That(asResult.Error.Message == "TestError2");
|
||||||
Assert.AreEqual(asResult.ResponseStatusCode, System.Net.HttpStatusCode.OK);
|
Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK);
|
||||||
Assert.AreEqual(asResult.ResponseTime, TimeSpan.FromSeconds(1));
|
Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1));
|
||||||
Assert.AreEqual(asResult.RequestUrl, "https://test.com/api");
|
Assert.That(asResult.RequestUrl == "https://test.com/api");
|
||||||
Assert.AreEqual(asResult.RequestMethod, HttpMethod.Get);
|
Assert.That(asResult.RequestMethod == HttpMethod.Get);
|
||||||
Assert.IsNull(asResult.Data);
|
ClassicAssert.IsNull(asResult.Data);
|
||||||
Assert.IsFalse(asResult);
|
ClassicAssert.IsFalse(asResult);
|
||||||
Assert.IsFalse(asResult.Success);
|
ClassicAssert.IsFalse(asResult.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -153,14 +154,14 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
null);
|
null);
|
||||||
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
||||||
|
|
||||||
Assert.IsNull(asResult.Error);
|
ClassicAssert.IsNull(asResult.Error);
|
||||||
Assert.AreEqual(asResult.ResponseStatusCode, System.Net.HttpStatusCode.OK);
|
Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK);
|
||||||
Assert.AreEqual(asResult.ResponseTime, TimeSpan.FromSeconds(1));
|
Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1));
|
||||||
Assert.AreEqual(asResult.RequestUrl, "https://test.com/api");
|
Assert.That(asResult.RequestUrl == "https://test.com/api");
|
||||||
Assert.AreEqual(asResult.RequestMethod, HttpMethod.Get);
|
Assert.That(asResult.RequestMethod == HttpMethod.Get);
|
||||||
Assert.IsNotNull(asResult.Data);
|
ClassicAssert.IsNotNull(asResult.Data);
|
||||||
Assert.IsTrue(asResult);
|
Assert.That(asResult);
|
||||||
Assert.IsTrue(asResult.Success);
|
Assert.That(asResult.Success);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0-preview-20211130-02"></PackageReference>
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0"></PackageReference>
|
||||||
<PackageReference Include="Moq" Version="4.16.1" />
|
<PackageReference Include="Moq" Version="4.20.70" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.2"></PackageReference>
|
<PackageReference Include="NUnit" Version="4.1.0"></PackageReference>
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.0"></PackageReference>
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0"></PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NUnit.Framework.Legacy;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
@@ -16,7 +17,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void ClampValueTests(decimal min, decimal max, decimal input, decimal expected)
|
public void ClampValueTests(decimal min, decimal max, decimal input, decimal expected)
|
||||||
{
|
{
|
||||||
var result = ExchangeHelpers.ClampValue(min, max, input);
|
var result = ExchangeHelpers.ClampValue(min, max, input);
|
||||||
Assert.AreEqual(expected, result);
|
Assert.That(expected == result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(0.1, 1, 0.1, RoundingType.Down, 0.4, 0.4)]
|
[TestCase(0.1, 1, 0.1, RoundingType.Down, 0.4, 0.4)]
|
||||||
@@ -33,7 +34,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected)
|
public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected)
|
||||||
{
|
{
|
||||||
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
|
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
|
||||||
Assert.AreEqual(expected, result);
|
Assert.That(expected == result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(0.1, 1, 2, RoundingType.Closest, 0.4, 0.4)]
|
[TestCase(0.1, 1, 2, RoundingType.Closest, 0.4, 0.4)]
|
||||||
@@ -48,7 +49,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void AdjustValuePrecisionTests(decimal min, decimal max, int? precision, RoundingType roundingType, decimal input, decimal expected)
|
public void AdjustValuePrecisionTests(decimal min, decimal max, int? precision, RoundingType roundingType, decimal input, decimal expected)
|
||||||
{
|
{
|
||||||
var result = ExchangeHelpers.AdjustValuePrecision(min, max, precision, roundingType, input);
|
var result = ExchangeHelpers.AdjustValuePrecision(min, max, precision, roundingType, input);
|
||||||
Assert.AreEqual(expected, result);
|
Assert.That(expected == result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(5, 0.1563158, 0.15631)]
|
[TestCase(5, 0.1563158, 0.15631)]
|
||||||
@@ -59,7 +60,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void RoundDownTests(int decimalPlaces, decimal input, decimal expected)
|
public void RoundDownTests(int decimalPlaces, decimal input, decimal expected)
|
||||||
{
|
{
|
||||||
var result = ExchangeHelpers.RoundDown(input, decimalPlaces);
|
var result = ExchangeHelpers.RoundDown(input, decimalPlaces);
|
||||||
Assert.AreEqual(expected, result);
|
Assert.That(expected == result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(0.1234560000, "0.123456")]
|
[TestCase(0.1234560000, "0.123456")]
|
||||||
@@ -67,7 +68,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void NormalizeTests(decimal input, string expected)
|
public void NormalizeTests(decimal input, string expected)
|
||||||
{
|
{
|
||||||
var result = ExchangeHelpers.Normalize(input);
|
var result = ExchangeHelpers.Normalize(input);
|
||||||
Assert.AreEqual(expected, result.ToString(CultureInfo.InvariantCulture));
|
Assert.That(expected == result.ToString(CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-19
@@ -1,7 +1,9 @@
|
|||||||
using CryptoExchange.Net.Attributes;
|
using CryptoExchange.Net.Attributes;
|
||||||
using CryptoExchange.Net.Converters;
|
using CryptoExchange.Net.Converters;
|
||||||
|
using CryptoExchange.Net.Converters.JsonNet;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NUnit.Framework.Legacy;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -11,7 +13,7 @@ using System.Threading.Tasks;
|
|||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
[TestFixture()]
|
[TestFixture()]
|
||||||
public class ConverterTests
|
public class JsonNetConverterTests
|
||||||
{
|
{
|
||||||
[TestCase("2021-05-12")]
|
[TestCase("2021-05-12")]
|
||||||
[TestCase("20210512")]
|
[TestCase("20210512")]
|
||||||
@@ -27,7 +29,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void TestDateTimeConverterString(string input, bool expectNull = false)
|
public void TestDateTimeConverterString(string input, bool expectNull = false)
|
||||||
{
|
{
|
||||||
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": \"{input}\" }}");
|
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": \"{input}\" }}");
|
||||||
Assert.AreEqual(output.Time, expectNull ? null: new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
Assert.That(output.Time == (expectNull ? null: new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(1620777600.000)]
|
[TestCase(1620777600.000)]
|
||||||
@@ -35,7 +37,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void TestDateTimeConverterDouble(double input)
|
public void TestDateTimeConverterDouble(double input)
|
||||||
{
|
{
|
||||||
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": {input} }}");
|
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": {input} }}");
|
||||||
Assert.AreEqual(output.Time, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
Assert.That(output.Time == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(1620777600)]
|
[TestCase(1620777600)]
|
||||||
@@ -46,7 +48,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void TestDateTimeConverterLong(long input, bool expectNull = false)
|
public void TestDateTimeConverterLong(long input, bool expectNull = false)
|
||||||
{
|
{
|
||||||
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": {input} }}");
|
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": {input} }}");
|
||||||
Assert.AreEqual(output.Time, expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
Assert.That(output.Time == (expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(1620777600)]
|
[TestCase(1620777600)]
|
||||||
@@ -54,14 +56,14 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void TestDateTimeConverterFromSeconds(double input)
|
public void TestDateTimeConverterFromSeconds(double input)
|
||||||
{
|
{
|
||||||
var output = DateTimeConverter.ConvertFromSeconds(input);
|
var output = DateTimeConverter.ConvertFromSeconds(input);
|
||||||
Assert.AreEqual(output, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestDateTimeConverterToSeconds()
|
public void TestDateTimeConverterToSeconds()
|
||||||
{
|
{
|
||||||
var output = DateTimeConverter.ConvertToSeconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
var output = DateTimeConverter.ConvertToSeconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||||
Assert.AreEqual(output, 1620777600);
|
Assert.That(output == 1620777600);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(1620777600000)]
|
[TestCase(1620777600000)]
|
||||||
@@ -69,49 +71,49 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void TestDateTimeConverterFromMilliseconds(double input)
|
public void TestDateTimeConverterFromMilliseconds(double input)
|
||||||
{
|
{
|
||||||
var output = DateTimeConverter.ConvertFromMilliseconds(input);
|
var output = DateTimeConverter.ConvertFromMilliseconds(input);
|
||||||
Assert.AreEqual(output, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestDateTimeConverterToMilliseconds()
|
public void TestDateTimeConverterToMilliseconds()
|
||||||
{
|
{
|
||||||
var output = DateTimeConverter.ConvertToMilliseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
var output = DateTimeConverter.ConvertToMilliseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||||
Assert.AreEqual(output, 1620777600000);
|
Assert.That(output == 1620777600000);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(1620777600000000)]
|
[TestCase(1620777600000000)]
|
||||||
public void TestDateTimeConverterFromMicroseconds(long input)
|
public void TestDateTimeConverterFromMicroseconds(long input)
|
||||||
{
|
{
|
||||||
var output = DateTimeConverter.ConvertFromMicroseconds(input);
|
var output = DateTimeConverter.ConvertFromMicroseconds(input);
|
||||||
Assert.AreEqual(output, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestDateTimeConverterToMicroseconds()
|
public void TestDateTimeConverterToMicroseconds()
|
||||||
{
|
{
|
||||||
var output = DateTimeConverter.ConvertToMicroseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
var output = DateTimeConverter.ConvertToMicroseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||||
Assert.AreEqual(output, 1620777600000000);
|
Assert.That(output == 1620777600000000);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(1620777600000000000)]
|
[TestCase(1620777600000000000)]
|
||||||
public void TestDateTimeConverterFromNanoseconds(long input)
|
public void TestDateTimeConverterFromNanoseconds(long input)
|
||||||
{
|
{
|
||||||
var output = DateTimeConverter.ConvertFromNanoseconds(input);
|
var output = DateTimeConverter.ConvertFromNanoseconds(input);
|
||||||
Assert.AreEqual(output, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestDateTimeConverterToNanoseconds()
|
public void TestDateTimeConverterToNanoseconds()
|
||||||
{
|
{
|
||||||
var output = DateTimeConverter.ConvertToNanoseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
var output = DateTimeConverter.ConvertToNanoseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||||
Assert.AreEqual(output, 1620777600000000000);
|
Assert.That(output == 1620777600000000000);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase()]
|
[TestCase()]
|
||||||
public void TestDateTimeConverterNull()
|
public void TestDateTimeConverterNull()
|
||||||
{
|
{
|
||||||
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": null }}");
|
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": null }}");
|
||||||
Assert.AreEqual(output.Time, null);
|
Assert.That(output.Time == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(TestEnum.One, "1")]
|
[TestCase(TestEnum.One, "1")]
|
||||||
@@ -122,7 +124,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void TestEnumConverterNullableGetStringTests(TestEnum? value, string expected)
|
public void TestEnumConverterNullableGetStringTests(TestEnum? value, string expected)
|
||||||
{
|
{
|
||||||
var output = EnumConverter.GetString(value);
|
var output = EnumConverter.GetString(value);
|
||||||
Assert.AreEqual(output, expected);
|
Assert.That(output == expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(TestEnum.One, "1")]
|
[TestCase(TestEnum.One, "1")]
|
||||||
@@ -132,7 +134,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void TestEnumConverterGetStringTests(TestEnum value, string expected)
|
public void TestEnumConverterGetStringTests(TestEnum value, string expected)
|
||||||
{
|
{
|
||||||
var output = EnumConverter.GetString(value);
|
var output = EnumConverter.GetString(value);
|
||||||
Assert.AreEqual(output, expected);
|
Assert.That(output == expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("1", TestEnum.One)]
|
[TestCase("1", TestEnum.One)]
|
||||||
@@ -147,7 +149,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var val = value == null ? "null" : $"\"{value}\"";
|
var val = value == null ? "null" : $"\"{value}\"";
|
||||||
var output = JsonConvert.DeserializeObject<EnumObject>($"{{ \"Value\": {val} }}");
|
var output = JsonConvert.DeserializeObject<EnumObject>($"{{ \"Value\": {val} }}");
|
||||||
Assert.AreEqual(output.Value, expected);
|
Assert.That(output.Value == expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("1", TestEnum.One)]
|
[TestCase("1", TestEnum.One)]
|
||||||
@@ -162,7 +164,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var val = value == null ? "null" : $"\"{value}\"";
|
var val = value == null ? "null" : $"\"{value}\"";
|
||||||
var output = JsonConvert.DeserializeObject<NotNullableEnumObject>($"{{ \"Value\": {val} }}");
|
var output = JsonConvert.DeserializeObject<NotNullableEnumObject>($"{{ \"Value\": {val} }}");
|
||||||
Assert.AreEqual(output.Value, expected);
|
Assert.That(output.Value == expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("1", true)]
|
[TestCase("1", true)]
|
||||||
@@ -181,7 +183,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var val = value == null ? "null" : $"\"{value}\"";
|
var val = value == null ? "null" : $"\"{value}\"";
|
||||||
var output = JsonConvert.DeserializeObject<BoolObject>($"{{ \"Value\": {val} }}");
|
var output = JsonConvert.DeserializeObject<BoolObject>($"{{ \"Value\": {val} }}");
|
||||||
Assert.AreEqual(output.Value, expected);
|
Assert.That(output.Value == expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("1", true)]
|
[TestCase("1", true)]
|
||||||
@@ -200,7 +202,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var val = value == null ? "null" : $"\"{value}\"";
|
var val = value == null ? "null" : $"\"{value}\"";
|
||||||
var output = JsonConvert.DeserializeObject<NotNullableBoolObject>($"{{ \"Value\": {val} }}");
|
var output = JsonConvert.DeserializeObject<NotNullableBoolObject>($"{{ \"Value\": {val} }}");
|
||||||
Assert.AreEqual(output.Value, expected);
|
Assert.That(output.Value == expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4,6 +4,7 @@ using CryptoExchange.Net.Objects.Options;
|
|||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NUnit.Framework.Legacy;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -49,9 +50,9 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
};
|
};
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.AreEqual(options.ReceiveWindow, TimeSpan.FromSeconds(10));
|
Assert.That(options.ReceiveWindow == TimeSpan.FromSeconds(10));
|
||||||
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
|
Assert.That(options.ApiCredentials.Key.GetString() == "123");
|
||||||
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
|
Assert.That(options.ApiCredentials.Secret.GetString() == "456");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -63,10 +64,10 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
options.Api2Options.ApiCredentials = new ApiCredentials("789", "101");
|
options.Api2Options.ApiCredentials = new ApiCredentials("789", "101");
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "123");
|
Assert.That(options.Api1Options.ApiCredentials.Key.GetString() == "123");
|
||||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "456");
|
Assert.That(options.Api1Options.ApiCredentials.Secret.GetString() == "456");
|
||||||
Assert.AreEqual(options.Api2Options.ApiCredentials.Key.GetString(), "789");
|
Assert.That(options.Api2Options.ApiCredentials.Key.GetString() == "789");
|
||||||
Assert.AreEqual(options.Api2Options.ApiCredentials.Secret.GetString(), "101");
|
Assert.That(options.Api2Options.ApiCredentials.Secret.GetString() == "101");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -79,10 +80,10 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||||
Assert.AreEqual(authProvider1.GetKey(), "111");
|
Assert.That(authProvider1.GetKey() == "111");
|
||||||
Assert.AreEqual(authProvider1.GetSecret(), "222");
|
Assert.That(authProvider1.GetSecret() == "222");
|
||||||
Assert.AreEqual(authProvider2.GetKey(), "333");
|
Assert.That(authProvider2.GetKey() == "333");
|
||||||
Assert.AreEqual(authProvider2.GetSecret(), "444");
|
Assert.That(authProvider2.GetSecret() == "444");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -95,10 +96,10 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||||
Assert.AreEqual(authProvider1.GetKey(), "111");
|
Assert.That(authProvider1.GetKey() == "111");
|
||||||
Assert.AreEqual(authProvider1.GetSecret(), "222");
|
Assert.That(authProvider1.GetSecret() == "222");
|
||||||
Assert.AreEqual(authProvider2.GetKey(), "123");
|
Assert.That(authProvider2.GetKey() == "123");
|
||||||
Assert.AreEqual(authProvider2.GetSecret(), "456");
|
Assert.That(authProvider2.GetSecret() == "456");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -115,11 +116,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||||
Assert.AreEqual(authProvider1.GetKey(), "333");
|
Assert.That(authProvider1.GetKey() == "333");
|
||||||
Assert.AreEqual(authProvider1.GetSecret(), "444");
|
Assert.That(authProvider1.GetSecret() == "444");
|
||||||
Assert.AreEqual(authProvider2.GetKey(), "123");
|
Assert.That(authProvider2.GetKey() == "123");
|
||||||
Assert.AreEqual(authProvider2.GetSecret(), "456");
|
Assert.That(authProvider2.GetSecret() == "456");
|
||||||
Assert.AreEqual(client.Api2.BaseAddress, "https://localhost:123");
|
Assert.That(client.Api2.BaseAddress == "https://localhost:123");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ using Microsoft.Extensions.Logging;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using NUnit.Framework.Legacy;
|
||||||
|
using CryptoExchange.Net.RateLimiting;
|
||||||
|
using System.Net;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Guards;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Filters;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
@@ -30,8 +36,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var result = client.Api1.Request<TestObject>().Result;
|
var result = client.Api1.Request<TestObject>().Result;
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.IsTrue(result.Success);
|
Assert.That(result.Success);
|
||||||
Assert.IsTrue(TestHelpers.AreEqual(expected, result.Data));
|
Assert.That(TestHelpers.AreEqual(expected, result.Data));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
@@ -45,8 +51,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var result = client.Api1.Request<TestObject>().Result;
|
var result = client.Api1.Request<TestObject>().Result;
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.IsFalse(result.Success);
|
ClassicAssert.IsFalse(result.Success);
|
||||||
Assert.IsTrue(result.Error != null);
|
Assert.That(result.Error != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
@@ -60,8 +66,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var result = await client.Api1.Request<TestObject>();
|
var result = await client.Api1.Request<TestObject>();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.IsFalse(result.Success);
|
ClassicAssert.IsFalse(result.Success);
|
||||||
Assert.IsTrue(result.Error != null);
|
Assert.That(result.Error != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
@@ -75,11 +81,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var result = await client.Api1.Request<TestObject>();
|
var result = await client.Api1.Request<TestObject>();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.IsFalse(result.Success);
|
ClassicAssert.IsFalse(result.Success);
|
||||||
Assert.IsTrue(result.Error != null);
|
Assert.That(result.Error != null);
|
||||||
Assert.IsTrue(result.Error is ServerError);
|
Assert.That(result.Error is ServerError);
|
||||||
Assert.IsTrue(result.Error.Message.Contains("Invalid request"));
|
Assert.That(result.Error.Message.Contains("Invalid request"));
|
||||||
Assert.IsTrue(result.Error.Message.Contains("123"));
|
Assert.That(result.Error.Message.Contains("123"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
@@ -93,11 +99,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var result = await client.Api2.Request<TestObject>();
|
var result = await client.Api2.Request<TestObject>();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.IsFalse(result.Success);
|
ClassicAssert.IsFalse(result.Success);
|
||||||
Assert.IsTrue(result.Error != null);
|
Assert.That(result.Error != null);
|
||||||
Assert.IsTrue(result.Error is ServerError);
|
Assert.That(result.Error is ServerError);
|
||||||
Assert.IsTrue(result.Error.Code == 123);
|
Assert.That(result.Error.Code == 123);
|
||||||
Assert.IsTrue(result.Error.Message == "Invalid request");
|
Assert.That(result.Error.Message == "Invalid request");
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
@@ -106,15 +112,15 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
// arrange
|
// arrange
|
||||||
// act
|
// act
|
||||||
var options = new TestClientOptions();
|
var options = new TestClientOptions();
|
||||||
options.Api1Options.RateLimiters = new List<IRateLimiter> { new RateLimiter() };
|
options.Api1Options.TimestampRecalculationInterval = TimeSpan.FromMinutes(10);
|
||||||
options.Api1Options.RateLimitingBehaviour = RateLimitingBehaviour.Fail;
|
options.Api1Options.OutputOriginalData = true;
|
||||||
options.RequestTimeout = TimeSpan.FromMinutes(1);
|
options.RequestTimeout = TimeSpan.FromMinutes(1);
|
||||||
var client = new TestBaseClient(options);
|
var client = new TestBaseClient(options);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimiters.Count == 1);
|
Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.TimestampRecalculationInterval == TimeSpan.FromMinutes(10));
|
||||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimitingBehaviour == RateLimitingBehaviour.Fail);
|
Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.OutputOriginalData == true);
|
||||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).RequestTimeout == TimeSpan.FromMinutes(1));
|
Assert.That(((TestClientOptions)client.ClientOptions).RequestTimeout == TimeSpan.FromMinutes(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
|
[TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
|
||||||
@@ -145,13 +151,13 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
});
|
});
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.AreEqual(request.Method, new HttpMethod(method));
|
Assert.That(request.Method == new HttpMethod(method));
|
||||||
Assert.AreEqual(request.Content?.Contains("TestParam1") == true, pos == HttpMethodParameterPosition.InBody);
|
Assert.That((request.Content?.Contains("TestParam1") == true) == (pos == HttpMethodParameterPosition.InBody));
|
||||||
Assert.AreEqual(request.Uri.ToString().Contains("TestParam1"), pos == HttpMethodParameterPosition.InUri);
|
Assert.That((request.Uri.ToString().Contains("TestParam1")) == (pos == HttpMethodParameterPosition.InUri));
|
||||||
Assert.AreEqual(request.Content?.Contains("TestParam2") == true, pos == HttpMethodParameterPosition.InBody);
|
Assert.That((request.Content?.Contains("TestParam2") == true) == (pos == HttpMethodParameterPosition.InBody));
|
||||||
Assert.AreEqual(request.Uri.ToString().Contains("TestParam2"), pos == HttpMethodParameterPosition.InUri);
|
Assert.That((request.Uri.ToString().Contains("TestParam2")) == (pos == HttpMethodParameterPosition.InUri));
|
||||||
Assert.AreEqual(request.GetHeaders().First().Key, "TestHeader");
|
Assert.That(request.GetHeaders().First().Key == "TestHeader");
|
||||||
Assert.IsTrue(request.GetHeaders().First().Value.Contains("123"));
|
Assert.That(request.GetHeaders().First().Value.Contains("123"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -161,18 +167,22 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[TestCase(1, 2)]
|
[TestCase(1, 2)]
|
||||||
public async Task PartialEndpointRateLimiterBasics(int requests, double perSeconds)
|
public async Task PartialEndpointRateLimiterBasics(int requests, double perSeconds)
|
||||||
{
|
{
|
||||||
var rateLimiter = new RateLimiter();
|
var rateLimiter = new RateLimitGate("Test");
|
||||||
rateLimiter.AddPartialEndpointLimit("/sapi/", requests, TimeSpan.FromSeconds(perSeconds));
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new PathStartFilter("/sapi/"), requests, TimeSpan.FromSeconds(perSeconds), RateLimitWindowType.Fixed));
|
||||||
|
|
||||||
|
var triggered = false;
|
||||||
|
rateLimiter.RateLimitTriggered += (x) => { triggered = true; };
|
||||||
|
var requestDefinition = new RequestDefinition("/sapi/v1/system/status", HttpMethod.Get);
|
||||||
|
|
||||||
for (var i = 0; i < requests + 1; i++)
|
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);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||||
Assert.IsTrue(i == requests? result1.Data > 1 : result1.Data == 0);
|
Assert.That(i == requests? triggered : !triggered);
|
||||||
}
|
}
|
||||||
|
triggered = false;
|
||||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
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);
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||||
Assert.IsTrue(result2.Data == 0);
|
Assert.That(!triggered);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("/sapi/test1", true)]
|
[TestCase("/sapi/test1", true)]
|
||||||
@@ -182,29 +192,40 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[TestCase("/sapi/", true)]
|
[TestCase("/sapi/", true)]
|
||||||
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
|
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
|
||||||
{
|
{
|
||||||
var rateLimiter = new RateLimiter();
|
var rateLimiter = new RateLimitGate("Test");
|
||||||
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1));
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||||
|
|
||||||
|
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
|
||||||
|
|
||||||
|
RateLimitEvent evnt = null;
|
||||||
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||||
for (var i = 0; i < 2; i++)
|
for (var i = 0; i < 2; i++)
|
||||||
{
|
{
|
||||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||||
bool expected = i == 1 ? (expectLimiting ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
bool expected = i == 1 ? (expectLimiting ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
||||||
Assert.IsTrue(expected);
|
Assert.That(expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("/sapi/", "/sapi/", true)]
|
[TestCase("/sapi/", "/sapi/", true)]
|
||||||
[TestCase("/sapi/test", "/sapi/test", true)]
|
[TestCase("/sapi/test", "/sapi/test", true)]
|
||||||
[TestCase("/sapi/test", "/sapi/test123", false)]
|
[TestCase("/sapi/test", "/sapi/test123", false)]
|
||||||
[TestCase("/sapi/test", "/sapi/", false)]
|
[TestCase("/sapi/test", "/sapi/", false)]
|
||||||
public async Task PartialEndpointRateLimiterEndpoints(string endpoint1, string endpoint2, bool expectLimiting)
|
public async Task PartialEndpointRateLimiterEndpoints(string endpoint1, string endpoint2, bool expectLimiting)
|
||||||
{
|
{
|
||||||
var rateLimiter = new RateLimiter();
|
var rateLimiter = new RateLimitGate("Test");
|
||||||
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1), countPerEndpoint: true);
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||||
|
|
||||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
||||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get);
|
||||||
Assert.IsTrue(result1.Data == 0);
|
|
||||||
Assert.IsTrue(expectLimiting ? result2.Data > 0 : result2.Data == 0);
|
RateLimitEvent evnt = null;
|
||||||
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||||
|
|
||||||
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||||
|
Assert.That(evnt == null);
|
||||||
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||||
|
Assert.That(expectLimiting ? evnt != null : evnt == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(1, 0.1)]
|
[TestCase(1, 0.1)]
|
||||||
@@ -213,18 +234,22 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[TestCase(1, 2)]
|
[TestCase(1, 2)]
|
||||||
public async Task EndpointRateLimiterBasics(int requests, double perSeconds)
|
public async Task EndpointRateLimiterBasics(int requests, double perSeconds)
|
||||||
{
|
{
|
||||||
var rateLimiter = new RateLimiter();
|
var rateLimiter = new RateLimitGate("Test");
|
||||||
rateLimiter.AddEndpointLimit("/sapi/test", requests, TimeSpan.FromSeconds(perSeconds));
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/test"), requests, TimeSpan.FromSeconds(perSeconds), RateLimitWindowType.Fixed));
|
||||||
|
|
||||||
|
bool triggered = false;
|
||||||
|
rateLimiter.RateLimitTriggered += (x) => { triggered = true; };
|
||||||
|
var requestDefinition = new RequestDefinition("/sapi/test", HttpMethod.Get);
|
||||||
|
|
||||||
for (var i = 0; i < requests + 1; i++)
|
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);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||||
Assert.IsTrue(i == requests ? result1.Data > 1 : result1.Data == 0);
|
Assert.That(i == requests ? triggered : !triggered);
|
||||||
}
|
}
|
||||||
|
triggered = false;
|
||||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
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);
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||||
Assert.IsTrue(result2.Data == 0);
|
Assert.That(!triggered);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("/", false)]
|
[TestCase("/", false)]
|
||||||
@@ -232,14 +257,18 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[TestCase("/sapi/test/123", false)]
|
[TestCase("/sapi/test/123", false)]
|
||||||
public async Task EndpointRateLimiterEndpoints(string endpoint, bool expectLimited)
|
public async Task EndpointRateLimiterEndpoints(string endpoint, bool expectLimited)
|
||||||
{
|
{
|
||||||
var rateLimiter = new RateLimiter();
|
var rateLimiter = new RateLimitGate("Test");
|
||||||
rateLimiter.AddEndpointLimit("/sapi/test", 1, TimeSpan.FromSeconds(0.1));
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathFilter("/sapi/test"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||||
|
|
||||||
|
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
|
||||||
|
|
||||||
|
RateLimitEvent evnt = null;
|
||||||
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||||
for (var i = 0; i < 2; i++)
|
for (var i = 0; i < 2; i++)
|
||||||
{
|
{
|
||||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||||
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
||||||
Assert.IsTrue(expected);
|
Assert.That(expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,47 +278,41 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[TestCase("/sapi/test23", false)]
|
[TestCase("/sapi/test23", false)]
|
||||||
public async Task EndpointRateLimiterMultipleEndpoints(string endpoint, bool expectLimited)
|
public async Task EndpointRateLimiterMultipleEndpoints(string endpoint, bool expectLimited)
|
||||||
{
|
{
|
||||||
var rateLimiter = new RateLimiter();
|
var rateLimiter = new RateLimitGate("Test");
|
||||||
rateLimiter.AddEndpointLimit(new[] { "/sapi/test", "/sapi/test2" }, 1, TimeSpan.FromSeconds(0.1));
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathsFilter(new[] { "/sapi/test", "/sapi/test2" }), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||||
|
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
|
||||||
|
|
||||||
|
RateLimitEvent evnt = null;
|
||||||
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||||
for (var i = 0; i < 2; i++)
|
for (var i = 0; i < 2; i++)
|
||||||
{
|
{
|
||||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||||
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
||||||
Assert.IsTrue(expected);
|
Assert.That(expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, true, true, true)]
|
[TestCase("123", "123", "/sapi/test", "/sapi/test", true)]
|
||||||
[TestCase("123", "456", "/sapi/test", "/sapi/test", true, true, true, false)]
|
[TestCase("123", "456", "/sapi/test", "/sapi/test", false)]
|
||||||
[TestCase("123", "123", "/sapi/test", "/sapi/test2", true, true, true, true)]
|
[TestCase("123", "123", "/sapi/test", "/sapi/test2", true)]
|
||||||
[TestCase("123", "123", "/sapi/test2", "/sapi/test", true, true, true, true)]
|
[TestCase("123", "123", "/sapi/test2", "/sapi/test", true)]
|
||||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, false, true, false)]
|
[TestCase(null, "123", "/sapi/test", "/sapi/test", false)]
|
||||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, true, true, false)]
|
[TestCase("123", null, "/sapi/test", "/sapi/test", false)]
|
||||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, false, true, false)]
|
[TestCase(null, null, "/sapi/test", "/sapi/test", false)]
|
||||||
[TestCase(null, "123", "/sapi/test", "/sapi/test", false, true, true, false)]
|
public async Task ApiKeyRateLimiterBasics(string key1, string key2, string endpoint1, string endpoint2, bool expectLimited)
|
||||||
[TestCase("123", null, "/sapi/test", "/sapi/test", true, false, true, false)]
|
|
||||||
[TestCase(null, null, "/sapi/test", "/sapi/test", false, false, true, false)]
|
|
||||||
|
|
||||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, true, false, true)]
|
|
||||||
[TestCase("123", "456", "/sapi/test", "/sapi/test", true, true, false, false)]
|
|
||||||
[TestCase("123", "123", "/sapi/test", "/sapi/test2", true, true, false, true)]
|
|
||||||
[TestCase("123", "123", "/sapi/test2", "/sapi/test", true, true, false, true)]
|
|
||||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, false, false, true)]
|
|
||||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, true, false, true)]
|
|
||||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, false, false, true)]
|
|
||||||
[TestCase(null, "123", "/sapi/test", "/sapi/test", false, true, false, false)]
|
|
||||||
[TestCase("123", null, "/sapi/test", "/sapi/test", true, false, false, false)]
|
|
||||||
[TestCase(null, null, "/sapi/test", "/sapi/test", false, false, false, true)]
|
|
||||||
public async Task ApiKeyRateLimiterBasics(string key1, string key2, string endpoint1, string endpoint2, bool signed1, bool signed2, bool onlyForSignedRequests, bool expectLimited)
|
|
||||||
{
|
{
|
||||||
var rateLimiter = new RateLimiter();
|
var rateLimiter = new RateLimitGate("Test");
|
||||||
rateLimiter.AddApiKeyLimit(1, TimeSpan.FromSeconds(0.1), onlyForSignedRequests, false);
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||||
|
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get) { Authenticated = key1 != null };
|
||||||
|
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
|
||||||
|
|
||||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, signed1, key1?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
RateLimitEvent evnt = null;
|
||||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, signed2, key2?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||||
Assert.IsTrue(result1.Data == 0);
|
|
||||||
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1?.ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||||
|
Assert.That(evnt == null);
|
||||||
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2?.ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||||
|
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("/sapi/test", "/sapi/test", true)]
|
[TestCase("/sapi/test", "/sapi/test", true)]
|
||||||
@@ -297,29 +320,55 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[TestCase("/", "/sapi/test2", true)]
|
[TestCase("/", "/sapi/test2", true)]
|
||||||
public async Task TotalRateLimiterBasics(string endpoint1, string endpoint2, bool expectLimited)
|
public async Task TotalRateLimiterBasics(string endpoint1, string endpoint2, bool expectLimited)
|
||||||
{
|
{
|
||||||
var rateLimiter = new RateLimiter();
|
var rateLimiter = new RateLimitGate("Test");
|
||||||
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, Array.Empty<IGuardFilter>(), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||||
|
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
||||||
|
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
|
||||||
|
|
||||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
RateLimitEvent evnt = null;
|
||||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, true, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||||
Assert.IsTrue(result1.Data == 0);
|
|
||||||
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||||
|
Assert.That(evnt == null);
|
||||||
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, default);
|
||||||
|
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("/sapi/test", true, true, true, false)]
|
[TestCase("https://test.com", "/sapi/test", "https://test.com", "/sapi/test", true)]
|
||||||
[TestCase("/sapi/test", false, true, true, false)]
|
[TestCase("https://test2.com", "/sapi/test", "https://test.com", "/sapi/test", false)]
|
||||||
[TestCase("/sapi/test", false, true, false, true)]
|
[TestCase("https://test.com", "/sapi/test", "https://test2.com", "/sapi/test", false)]
|
||||||
[TestCase("/sapi/test", true, true, false, true)]
|
[TestCase("https://test.com", "/sapi/test", "https://test.com", "/sapi/test2", true)]
|
||||||
public async Task ApiKeyRateLimiterIgnores_TotalRateLimiter_IfSet(string endpoint, bool signed1, bool signed2, bool ignoreTotal, bool expectLimited)
|
public async Task HostRateLimiterBasics(string host1, string endpoint1, string host2, string endpoint2, bool expectLimited)
|
||||||
{
|
{
|
||||||
var rateLimiter = new RateLimiter();
|
var rateLimiter = new RateLimitGate("Test");
|
||||||
rateLimiter.AddApiKeyLimit(100, TimeSpan.FromSeconds(0.1), true, ignoreTotal);
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new HostFilter("https://test.com"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||||
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
|
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
||||||
|
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
|
||||||
|
|
||||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, signed1, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
RateLimitEvent evnt = null;
|
||||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, signed2, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||||
Assert.IsTrue(result1.Data == 0);
|
|
||||||
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||||
|
Assert.That(evnt == null);
|
||||||
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||||
|
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("https://test.com", "https://test.com", true)]
|
||||||
|
[TestCase("https://test2.com", "https://test.com", false)]
|
||||||
|
[TestCase("https://test.com", "https://test2.com", false)]
|
||||||
|
public async Task ConnectionRateLimiterBasics(string host1, string host2, bool expectLimited)
|
||||||
|
{
|
||||||
|
var rateLimiter = new RateLimitGate("Test");
|
||||||
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||||
|
|
||||||
|
RateLimitEvent evnt = null;
|
||||||
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||||
|
|
||||||
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||||
|
Assert.That(evnt == null);
|
||||||
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||||
|
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ using CryptoExchange.Net.UnitTests.TestImplementations;
|
|||||||
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NUnit.Framework.Constraints;
|
using NUnit.Framework.Legacy;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
@@ -29,10 +29,9 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
options.SubOptions.MaxSocketConnections = 1;
|
options.SubOptions.MaxSocketConnections = 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
Assert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
|
ClassicAssert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
|
||||||
Assert.AreEqual(1, client.SubClient.ApiOptions.MaxSocketConnections);
|
Assert.That(1 == client.SubClient.ApiOptions.MaxSocketConnections);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(true)]
|
[TestCase(true)]
|
||||||
@@ -48,11 +47,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var connectResult = client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, null));
|
var connectResult = client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, null));
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
Assert.IsTrue(connectResult.Success == canConnect);
|
Assert.That(connectResult.Success == canConnect);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
public async Task SocketMessages_Should_BeProcessedInDataHandlers()
|
public void SocketMessages_Should_BeProcessedInDataHandlers()
|
||||||
{
|
{
|
||||||
// arrange
|
// arrange
|
||||||
var client = new TestSocketClient(options => {
|
var client = new TestSocketClient(options => {
|
||||||
@@ -68,23 +67,25 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
client.SubClient.ConnectSocketSub(sub);
|
client.SubClient.ConnectSocketSub(sub);
|
||||||
|
|
||||||
sub.AddSubscription(new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
||||||
{
|
{
|
||||||
result = messageEvent.Data;
|
result = messageEvent.Data;
|
||||||
rstEvent.Set();
|
rstEvent.Set();
|
||||||
}));
|
});
|
||||||
|
subObj.HandleUpdatesBeforeConfirmation = true;
|
||||||
|
sub.AddSubscription(subObj);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await socket.InvokeMessage("{\"property\": \"123\", \"topic\": \"topic\"}");
|
socket.InvokeMessage("{\"property\": \"123\", \"topic\": \"topic\"}");
|
||||||
rstEvent.WaitOne(1000);
|
rstEvent.WaitOne(1000);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.IsTrue(result["property"] == "123");
|
Assert.That(result["property"] == "123");
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(false)]
|
[TestCase(false)]
|
||||||
[TestCase(true)]
|
[TestCase(true)]
|
||||||
public async Task SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
|
public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
|
||||||
{
|
{
|
||||||
// arrange
|
// arrange
|
||||||
var client = new TestSocketClient(options =>
|
var client = new TestSocketClient(options =>
|
||||||
@@ -101,18 +102,21 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
string original = null;
|
string original = null;
|
||||||
|
|
||||||
client.SubClient.ConnectSocketSub(sub);
|
client.SubClient.ConnectSocketSub(sub);
|
||||||
sub.AddSubscription(new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
||||||
{
|
{
|
||||||
original = messageEvent.OriginalData;
|
original = messageEvent.OriginalData;
|
||||||
rstEvent.Set();
|
rstEvent.Set();
|
||||||
}));
|
});
|
||||||
|
subObj.HandleUpdatesBeforeConfirmation = true;
|
||||||
|
sub.AddSubscription(subObj);
|
||||||
|
var msgToSend = JsonConvert.SerializeObject(new { topic = "topic", property = 123 });
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await socket.InvokeMessage("{\"property\": 123}");
|
socket.InvokeMessage(msgToSend);
|
||||||
rstEvent.WaitOne(1000);
|
rstEvent.WaitOne(1000);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.IsTrue(original == (enabled ? "{\"property\": 123}" : null));
|
Assert.That(original == (enabled ? msgToSend : null));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase()]
|
[TestCase()]
|
||||||
@@ -136,7 +140,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
client.UnsubscribeAsync(ups).Wait();
|
client.UnsubscribeAsync(ups).Wait();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.IsTrue(socket.Connected == false);
|
Assert.That(socket.Connected == false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase()]
|
[TestCase()]
|
||||||
@@ -164,8 +168,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
client.UnsubscribeAllAsync().Wait();
|
client.UnsubscribeAllAsync().Wait();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.IsTrue(socket1.Connected == false);
|
Assert.That(socket1.Connected == false);
|
||||||
Assert.IsTrue(socket2.Connected == false);
|
Assert.That(socket2.Connected == false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase()]
|
[TestCase()]
|
||||||
@@ -181,7 +185,53 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var connectResult = client.SubClient.ConnectSocketSub(sub1);
|
var connectResult = client.SubClient.ConnectSocketSub(sub1);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.IsFalse(connectResult.Success);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using CryptoExchange.Net.Objects.Options;
|
|||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using CryptoExchange.Net.OrderBook;
|
using CryptoExchange.Net.OrderBook;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NUnit.Framework.Legacy;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
@@ -18,7 +19,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
private class TestableSymbolOrderBook : SymbolOrderBook
|
private class TestableSymbolOrderBook : SymbolOrderBook
|
||||||
{
|
{
|
||||||
public TestableSymbolOrderBook() : base(null, "Test", "BTC/USD")
|
public TestableSymbolOrderBook() : base(null, "Test", "Test", "BTC/USD")
|
||||||
{
|
{
|
||||||
Initialize(_defaultOrderBookOptions);
|
Initialize(_defaultOrderBookOptions);
|
||||||
}
|
}
|
||||||
@@ -56,31 +57,31 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void GivenEmptyBidList_WhenBestBid_ThenEmptySymbolOrderBookEntry()
|
public void GivenEmptyBidList_WhenBestBid_ThenEmptySymbolOrderBookEntry()
|
||||||
{
|
{
|
||||||
var symbolOrderBook = new TestableSymbolOrderBook();
|
var symbolOrderBook = new TestableSymbolOrderBook();
|
||||||
Assert.IsNotNull(symbolOrderBook.BestBid);
|
ClassicAssert.IsNotNull(symbolOrderBook.BestBid);
|
||||||
Assert.AreEqual(0m, symbolOrderBook.BestBid.Price);
|
Assert.That(0m == symbolOrderBook.BestBid.Price);
|
||||||
Assert.AreEqual(0m, symbolOrderBook.BestAsk.Quantity);
|
Assert.That(0m == symbolOrderBook.BestAsk.Quantity);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
public void GivenEmptyAskList_WhenBestAsk_ThenEmptySymbolOrderBookEntry()
|
public void GivenEmptyAskList_WhenBestAsk_ThenEmptySymbolOrderBookEntry()
|
||||||
{
|
{
|
||||||
var symbolOrderBook = new TestableSymbolOrderBook();
|
var symbolOrderBook = new TestableSymbolOrderBook();
|
||||||
Assert.IsNotNull(symbolOrderBook.BestBid);
|
ClassicAssert.IsNotNull(symbolOrderBook.BestBid);
|
||||||
Assert.AreEqual(0m, symbolOrderBook.BestBid.Price);
|
Assert.That(0m == symbolOrderBook.BestBid.Price);
|
||||||
Assert.AreEqual(0m, symbolOrderBook.BestAsk.Quantity);
|
Assert.That(0m == symbolOrderBook.BestAsk.Quantity);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
public void GivenEmptyBidAndAskList_WhenBestOffers_ThenEmptySymbolOrderBookEntries()
|
public void GivenEmptyBidAndAskList_WhenBestOffers_ThenEmptySymbolOrderBookEntries()
|
||||||
{
|
{
|
||||||
var symbolOrderBook = new TestableSymbolOrderBook();
|
var symbolOrderBook = new TestableSymbolOrderBook();
|
||||||
Assert.IsNotNull(symbolOrderBook.BestOffers);
|
ClassicAssert.IsNotNull(symbolOrderBook.BestOffers);
|
||||||
Assert.IsNotNull(symbolOrderBook.BestOffers.Bid);
|
ClassicAssert.IsNotNull(symbolOrderBook.BestOffers.Bid);
|
||||||
Assert.IsNotNull(symbolOrderBook.BestOffers.Ask);
|
ClassicAssert.IsNotNull(symbolOrderBook.BestOffers.Ask);
|
||||||
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Bid.Price);
|
Assert.That(0m == symbolOrderBook.BestOffers.Bid.Price);
|
||||||
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Bid.Quantity);
|
Assert.That(0m == symbolOrderBook.BestOffers.Bid.Quantity);
|
||||||
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Ask.Price);
|
Assert.That(0m == symbolOrderBook.BestOffers.Ask.Price);
|
||||||
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Ask.Quantity);
|
Assert.That(0m == symbolOrderBook.BestOffers.Ask.Quantity);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
@@ -103,12 +104,12 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var resultBids2 = orderbook.CalculateAverageFillPrice(1.5m, OrderBookEntryType.Bid);
|
var resultBids2 = orderbook.CalculateAverageFillPrice(1.5m, OrderBookEntryType.Bid);
|
||||||
var resultAsks2 = orderbook.CalculateAverageFillPrice(1.5m, OrderBookEntryType.Ask);
|
var resultAsks2 = orderbook.CalculateAverageFillPrice(1.5m, OrderBookEntryType.Ask);
|
||||||
|
|
||||||
Assert.True(resultBids.Success);
|
Assert.That(resultBids.Success);
|
||||||
Assert.True(resultAsks.Success);
|
Assert.That(resultAsks.Success);
|
||||||
Assert.AreEqual(1.05m, resultBids.Data);
|
Assert.That(1.05m == resultBids.Data);
|
||||||
Assert.AreEqual(1.25m, resultAsks.Data);
|
Assert.That(1.25m == resultAsks.Data);
|
||||||
Assert.AreEqual(1.06666667m, resultBids2.Data);
|
Assert.That(1.06666667m == resultBids2.Data);
|
||||||
Assert.AreEqual(1.23333333m, resultAsks2.Data);
|
Assert.That(1.23333333m == resultAsks2.Data);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
@@ -131,12 +132,12 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var resultBids2 = orderbook.CalculateTradableAmount(1.5m, OrderBookEntryType.Bid);
|
var resultBids2 = orderbook.CalculateTradableAmount(1.5m, OrderBookEntryType.Bid);
|
||||||
var resultAsks2 = orderbook.CalculateTradableAmount(1.5m, OrderBookEntryType.Ask);
|
var resultAsks2 = orderbook.CalculateTradableAmount(1.5m, OrderBookEntryType.Ask);
|
||||||
|
|
||||||
Assert.True(resultBids.Success);
|
Assert.That(resultBids.Success);
|
||||||
Assert.True(resultAsks.Success);
|
Assert.That(resultAsks.Success);
|
||||||
Assert.AreEqual(1.9m, resultBids.Data);
|
Assert.That(1.9m == resultBids.Data);
|
||||||
Assert.AreEqual(1.61538462m, resultAsks.Data);
|
Assert.That(1.61538462m == resultAsks.Data);
|
||||||
Assert.AreEqual(1.4m, resultBids2.Data);
|
Assert.That(1.4m == resultBids2.Data);
|
||||||
Assert.AreEqual(1.23076923m, resultAsks2.Data);
|
Assert.That(1.23076923m == resultAsks2.Data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
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.Objects;
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -22,11 +22,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
|||||||
_handler = handler;
|
_handler = handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<CallResult> DoHandleMessageAsync(SocketConnection connection, DataEvent<object> message)
|
public override CallResult DoHandleMessage(SocketConnection connection, DataEvent<object> message)
|
||||||
{
|
{
|
||||||
var data = (T)message.Data;
|
var data = (T)message.Data;
|
||||||
_handler.Invoke(message.As(data));
|
_handler.Invoke(message.As(data));
|
||||||
return Task.FromResult(new CallResult(null));
|
return new CallResult(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Type GetMessageType(IMessageAccessor message) => typeof(T);
|
public override Type GetMessageType(IMessageAccessor message) => typeof(T);
|
||||||
|
|||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
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,8 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
|
using CryptoExchange.Net.Clients;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||||
@@ -39,7 +42,17 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public CallResult<T> Deserialize<T>(string data) => Deserialize<T>(data, null, null);
|
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 override TimeSpan? GetTimeOffset() => null;
|
public override TimeSpan? GetTimeOffset() => null;
|
||||||
public override TimeSyncInfo GetTimeSyncInfo() => null;
|
public override TimeSyncInfo GetTimeSyncInfo() => null;
|
||||||
@@ -53,7 +66,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, 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, RequestBodyFormat bodyFormat, out SortedDictionary<string, object> uriParameters, out SortedDictionary<string, object> bodyParameters, out Dictionary<string, string> headers)
|
||||||
{
|
{
|
||||||
bodyParameters = new SortedDictionary<string, object>();
|
bodyParameters = new SortedDictionary<string, object>();
|
||||||
uriParameters = new SortedDictionary<string, object>();
|
uriParameters = new SortedDictionary<string, object>();
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ using CryptoExchange.Net.Authentication;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using CryptoExchange.Net.Clients;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||||
{
|
{
|
||||||
@@ -138,12 +139,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
||||||
{
|
{
|
||||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
|
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct, requestWeight: 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CallResult<T>> RequestWithParams<T>(HttpMethod method, Dictionary<string, object> parameters, Dictionary<string, string> headers) where T : class
|
public async Task<CallResult<T>> RequestWithParams<T>(HttpMethod method, Dictionary<string, object> parameters, Dictionary<string, string> headers) where T : class
|
||||||
{
|
{
|
||||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), method, default, parameters, additionalHeaders: headers);
|
return await SendRequestAsync<T>(new Uri("http://www.test.com"), method, default, parameters, requestWeight: 0, additionalHeaders: headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
|
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
|
||||||
@@ -179,14 +180,14 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
||||||
{
|
{
|
||||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
|
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct, requestWeight: 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, string data)
|
protected override Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, IMessageAccessor accessor)
|
||||||
{
|
{
|
||||||
var errorData = ValidateJson(data);
|
var errorData = accessor.Deserialize<TestError>();
|
||||||
|
|
||||||
return new ServerError((int)errorData.Data["errorCode"], (string)errorData.Data["errorMessage"]);
|
return new ServerError(errorData.Data.ErrorCode, errorData.Data.ErrorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override TimeSpan? GetTimeOffset()
|
public override TimeSpan? GetTimeOffset()
|
||||||
@@ -208,6 +209,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class TestError
|
||||||
|
{
|
||||||
|
public int ErrorCode { get; set; }
|
||||||
|
public string ErrorMessage { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class ParseErrorTestRestClient: TestRestClient
|
public class ParseErrorTestRestClient: TestRestClient
|
||||||
{
|
{
|
||||||
public ParseErrorTestRestClient() { }
|
public ParseErrorTestRestClient() { }
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
#pragma warning disable 0067
|
#pragma warning disable 0067
|
||||||
public event Func<Task> OnReconnected;
|
public event Func<Task> OnReconnected;
|
||||||
public event Func<Task> OnReconnecting;
|
public event Func<Task> OnReconnecting;
|
||||||
|
public event Func<int, Task> OnRequestRateLimited;
|
||||||
#pragma warning restore 0067
|
#pragma warning restore 0067
|
||||||
public event Func<int, Task> OnRequestSent;
|
public event Func<int, Task> OnRequestSent;
|
||||||
public event Func<WebSocketMessageType, Stream, Task> OnStreamMessage;
|
public event Action<WebSocketMessageType, ReadOnlyMemory<byte>> OnStreamMessage;
|
||||||
public event Func<Exception, Task> OnError;
|
public event Func<Exception, Task> OnError;
|
||||||
public event Func<Task> OnOpen;
|
public event Func<Task> OnOpen;
|
||||||
public Func<Task<Uri>> GetReconnectionUrl { get; set; }
|
public Func<Task<Uri>> GetReconnectionUrl { get; set; }
|
||||||
@@ -62,13 +63,13 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<bool> ConnectAsync()
|
public Task<CallResult> ConnectAsync()
|
||||||
{
|
{
|
||||||
Connected = CanConnect;
|
Connected = CanConnect;
|
||||||
ConnectCalls++;
|
ConnectCalls++;
|
||||||
if (CanConnect)
|
if (CanConnect)
|
||||||
InvokeOpen();
|
InvokeOpen();
|
||||||
return Task.FromResult(CanConnect);
|
return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Send(int requestId, string data, int weight)
|
public void Send(int requestId, string data, int weight)
|
||||||
@@ -111,10 +112,9 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
OnOpen?.Invoke();
|
OnOpen?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task InvokeMessage(string data)
|
public void InvokeMessage(string data)
|
||||||
{
|
{
|
||||||
var stream = new MemoryStream(Encoding.UTF8.GetBytes(data));
|
OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(data)));
|
||||||
await OnStreamMessage?.Invoke(WebSocketMessageType.Text, stream);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetProxy(ApiProxy proxy)
|
public void SetProxy(ApiProxy proxy)
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
|
using CryptoExchange.Net.Clients;
|
||||||
|
using CryptoExchange.Net.Converters.MessageParsing;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
|
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
@@ -71,8 +74,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
public class TestSubSocketClient : SocketApiClient
|
public class TestSubSocketClient : SocketApiClient
|
||||||
{
|
{
|
||||||
|
private MessagePath _channelPath = MessagePath.Get().Property("channel");
|
||||||
|
private MessagePath _topicPath = MessagePath.Get().Property("topic");
|
||||||
|
|
||||||
public TestSubSocketClient(TestSocketOptions options, SocketApiOptions apiOptions): base(new TraceLogger(), options.Environment.TestAddress, options, apiOptions)
|
public Subscription TestSubscription { get; private set; } = null;
|
||||||
|
|
||||||
|
public TestSubSocketClient(TestSocketOptions options, SocketApiOptions apiOptions) : base(new TraceLogger(), options.Environment.TestAddress, options, apiOptions)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -85,11 +92,28 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||||
=> new TestAuthProvider(credentials);
|
=> new TestAuthProvider(credentials);
|
||||||
|
|
||||||
public CallResult<bool> ConnectSocketSub(SocketConnection sub)
|
public CallResult ConnectSocketSub(SocketConnection sub)
|
||||||
{
|
{
|
||||||
return ConnectSocketAsync(sub).Result;
|
return ConnectSocketAsync(sub).Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string GetListenerIdentifier(IMessageAccessor messageAccessor) => "topic";
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Security;
|
using System.Security;
|
||||||
using System.Text;
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
using Newtonsoft.Json.Linq;
|
using CryptoExchange.Net.Converters.MessageParsing;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Authentication
|
namespace CryptoExchange.Net.Authentication
|
||||||
{
|
{
|
||||||
@@ -94,38 +94,21 @@ 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>
|
/// <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)
|
public ApiCredentials(Stream inputStream, string? identifierKey = null, string? identifierSecret = null)
|
||||||
{
|
{
|
||||||
using var reader = new StreamReader(inputStream, Encoding.UTF8, false, 512, true);
|
var accessor = new SystemTextJsonStreamMessageAccessor();
|
||||||
|
if (!accessor.Read(inputStream, false).Result)
|
||||||
var stringData = reader.ReadToEnd();
|
|
||||||
var jsonData = stringData.ToJToken();
|
|
||||||
if(jsonData == null)
|
|
||||||
throw new ArgumentException("Input stream not valid json data");
|
throw new ArgumentException("Input stream not valid json data");
|
||||||
|
|
||||||
var key = TryGetValue(jsonData, identifierKey ?? "apiKey");
|
var key = accessor.GetValue<string>(MessagePath.Get().Property(identifierKey ?? "apiKey"));
|
||||||
var secret = TryGetValue(jsonData, identifierSecret ?? "apiSecret");
|
var secret = accessor.GetValue<string>(MessagePath.Get().Property(identifierSecret ?? "apiSecret"));
|
||||||
|
|
||||||
if (key == null || secret == null)
|
if (key == null || secret == null)
|
||||||
throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");
|
throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");
|
||||||
|
|
||||||
Key = key.ToSecureString();
|
Key = key.ToSecureString();
|
||||||
Secret = secret.ToSecureString();
|
Secret = secret.ToSecureString();
|
||||||
|
|
||||||
inputStream.Seek(0, SeekOrigin.Begin);
|
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>
|
/// <summary>
|
||||||
/// Dispose
|
/// Dispose
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using CryptoExchange.Net.Converters;
|
using CryptoExchange.Net.Clients;
|
||||||
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -47,6 +48,7 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <param name="auth">If the requests should be authenticated</param>
|
/// <param name="auth">If the requests should be authenticated</param>
|
||||||
/// <param name="arraySerialization">Array serialization type</param>
|
/// <param name="arraySerialization">Array serialization type</param>
|
||||||
/// <param name="parameterPosition">The position where the providedParameters should go</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="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="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>
|
/// <param name="headers">The headers that should be send with the request</param>
|
||||||
@@ -58,6 +60,7 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
bool auth,
|
bool auth,
|
||||||
ArrayParametersSerialization arraySerialization,
|
ArrayParametersSerialization arraySerialization,
|
||||||
HttpMethodParameterPosition parameterPosition,
|
HttpMethodParameterPosition parameterPosition,
|
||||||
|
RequestBodyFormat requestBodyFormat,
|
||||||
out SortedDictionary<string, object> uriParameters,
|
out SortedDictionary<string, object> uriParameters,
|
||||||
out SortedDictionary<string, object> bodyParameters,
|
out SortedDictionary<string, object> bodyParameters,
|
||||||
out Dictionary<string, string> headers
|
out Dictionary<string, string> headers
|
||||||
@@ -245,7 +248,7 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// HMACSHA512 sign the data and return the hash
|
/// HMACSHA256 sign the data and return the hash
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="data">Data to sign</param>
|
/// <param name="data">Data to sign</param>
|
||||||
/// <param name="outputType">String type</param>
|
/// <param name="outputType">String type</param>
|
||||||
@@ -267,7 +270,7 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// HMACSHA512 sign the data and return the hash
|
/// HMACSHA384 sign the data and return the hash
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="data">Data to sign</param>
|
/// <param name="data">Data to sign</param>
|
||||||
/// <param name="outputType">String type</param>
|
/// <param name="outputType">String type</param>
|
||||||
|
|||||||
@@ -1,19 +1,10 @@
|
|||||||
using System;
|
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.Authentication;
|
||||||
using CryptoExchange.Net.Converters;
|
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net.Clients
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base API for all API clients
|
/// Base API for all API clients
|
||||||
@@ -35,37 +26,6 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public AuthenticationProvider? AuthenticationProvider { get; private set; }
|
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>
|
/// <summary>
|
||||||
/// The environment this client communicates to
|
/// The environment this client communicates to
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -76,11 +36,6 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool OutputOriginalData { get; }
|
public bool OutputOriginalData { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The default serializer
|
|
||||||
/// </summary>
|
|
||||||
protected virtual JsonSerializer DefaultSerializer { get; set; } = JsonSerializer.Create(SerializerOptions.Default);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Api options
|
/// Api options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -133,197 +88,6 @@ namespace CryptoExchange.Net
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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>
|
/// <summary>
|
||||||
/// Dispose
|
/// Dispose
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net.Clients
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The base for all clients, websocket client and rest client
|
/// The base for all clients, websocket client and rest client
|
||||||
@@ -15,7 +15,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The name of the API the client is for
|
/// The name of the API the client is for
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal string Name { get; }
|
public string Exchange { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Api clients in this client
|
/// Api clients in this client
|
||||||
@@ -26,7 +26,7 @@ namespace CryptoExchange.Net
|
|||||||
/// The log object
|
/// The log object
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected internal ILogger _logger;
|
protected internal ILogger _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provided client options
|
/// Provided client options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -36,14 +36,14 @@ namespace CryptoExchange.Net
|
|||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">Logger</param>
|
/// <param name="logger">Logger</param>
|
||||||
/// <param name="name">The name of the API this client is for</param>
|
/// <param name="exchange">The name of the exchange this client is for</param>
|
||||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||||
protected BaseClient(ILoggerFactory? logger, string name)
|
protected BaseClient(ILoggerFactory? logger, string exchange)
|
||||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||||
{
|
{
|
||||||
_logger = logger?.CreateLogger(name) ?? NullLoggerFactory.Instance.CreateLogger(name);
|
_logger = logger?.CreateLogger(exchange) ?? NullLoggerFactory.Instance.CreateLogger(exchange);
|
||||||
|
|
||||||
Name = name;
|
Exchange = exchange;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -57,7 +57,7 @@ namespace CryptoExchange.Net
|
|||||||
throw new ArgumentNullException(nameof(options));
|
throw new ArgumentNullException(nameof(options));
|
||||||
|
|
||||||
ClientOptions = options;
|
ClientOptions = options;
|
||||||
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {Name}.Net: v{GetType().Assembly.GetName().Version}");
|
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {Exchange}.Net: v{GetType().Assembly.GetName().Version}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -74,7 +74,7 @@ namespace CryptoExchange.Net
|
|||||||
/// Register an API client
|
/// Register an API client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="apiClient">The client</param>
|
/// <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)
|
if (ClientOptions == null)
|
||||||
throw new InvalidOperationException("Client should have called Initialize before adding API clients");
|
throw new InvalidOperationException("Client should have called Initialize before adding API clients");
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ using System.Linq;
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net.Clients
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base rest client
|
/// Base rest client
|
||||||
|
|||||||
@@ -4,23 +4,24 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Logging.Extensions;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net.Clients
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base for socket client implementations
|
/// Base for socket client implementations
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class BaseSocketClient: BaseClient, ISocketClient
|
public abstract class BaseSocketClient : BaseClient, ISocketClient
|
||||||
{
|
{
|
||||||
#region fields
|
#region fields
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If client is disposing
|
/// If client is disposing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected bool _disposing;
|
protected bool _disposing;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public int CurrentConnections => ApiClients.OfType<SocketApiClient>().Sum(c => c.CurrentConnections);
|
public int CurrentConnections => ApiClients.OfType<SocketApiClient>().Sum(c => c.CurrentConnections);
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -33,8 +34,8 @@ namespace CryptoExchange.Net
|
|||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">Logger</param>
|
/// <param name="logger">Logger</param>
|
||||||
/// <param name="name">The name of the API this client is for</param>
|
/// <param name="exchange">The name of the exchange this client is for</param>
|
||||||
protected BaseSocketClient(ILoggerFactory? logger, string name) : base(logger, name)
|
protected BaseSocketClient(ILoggerFactory? logger, string exchange) : base(logger, exchange)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,11 +46,11 @@ namespace CryptoExchange.Net
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async Task UnsubscribeAsync(int subscriptionId)
|
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);
|
var result = await socket.UnsubscribeAsync(subscriptionId).ConfigureAwait(false);
|
||||||
if (result)
|
if (result)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +64,7 @@ namespace CryptoExchange.Net
|
|||||||
if (subscription == null)
|
if (subscription == null)
|
||||||
throw new ArgumentNullException(nameof(subscription));
|
throw new ArgumentNullException(nameof(subscription));
|
||||||
|
|
||||||
_logger.Log(LogLevel.Information, $"Socket {subscription.SocketId} Unsubscribing subscription " + subscription.Id);
|
_logger.UnsubscribingSubscription(subscription.SocketId, subscription.Id);
|
||||||
await subscription.CloseAsync().ConfigureAwait(false);
|
await subscription.CloseAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,10 +74,10 @@ namespace CryptoExchange.Net
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async Task UnsubscribeAllAsync()
|
public virtual async Task UnsubscribeAllAsync()
|
||||||
{
|
{
|
||||||
var tasks = new List<Task>();
|
var tasks = new List<Task>();
|
||||||
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
||||||
tasks.Add(client.UnsubscribeAllAsync());
|
tasks.Add(client.UnsubscribeAllAsync());
|
||||||
|
|
||||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +87,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async Task ReconnectAsync()
|
public virtual async Task ReconnectAsync()
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Information, $"Reconnecting all {CurrentConnections} connections");
|
_logger.ReconnectingAllConnections(CurrentConnections);
|
||||||
var tasks = new List<Task>();
|
var tasks = new List<Task>();
|
||||||
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
||||||
{
|
{
|
||||||
@@ -107,5 +108,19 @@ namespace CryptoExchange.Net
|
|||||||
}
|
}
|
||||||
return result.ToString();
|
return result.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the state of all socket api clients
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public List<SocketApiClient.SocketApiClientState> GetSocketApiClientStates()
|
||||||
|
{
|
||||||
|
var result = new List<SocketApiClient.SocketApiClientState>();
|
||||||
|
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
||||||
|
{
|
||||||
|
result.Add(client.GetState());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="exchangeName"></param>
|
/// <param name="exchangeName"></param>
|
||||||
/// <returns></returns>
|
/// <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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,15 +8,17 @@ using System.Net;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using CryptoExchange.Net.Converters.JsonNet;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Logging.Extensions;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
|
using CryptoExchange.Net.RateLimiting;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
using CryptoExchange.Net.Requests;
|
using CryptoExchange.Net.Requests;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net.Clients
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base rest API client for interacting with a REST API
|
/// Base rest API client for interacting with a REST API
|
||||||
@@ -35,15 +37,36 @@ namespace CryptoExchange.Net
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public int TotalRequestsMade { get; set; }
|
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>
|
/// <summary>
|
||||||
/// Request headers to be sent with each request
|
/// Request headers to be sent with each request
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
|
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// List of rate limiters
|
/// Where to put the parameters for requests with different Http methods
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal IEnumerable<IRateLimiter> RateLimiters { get; }
|
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 />
|
/// <inheritdoc />
|
||||||
public new RestExchangeOptions ClientOptions => (RestExchangeOptions)base.ClientOptions;
|
public new RestExchangeOptions ClientOptions => (RestExchangeOptions)base.ClientOptions;
|
||||||
@@ -51,6 +74,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions;
|
public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions;
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -59,22 +83,278 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="baseAddress">Base address for this API client</param>
|
/// <param name="baseAddress">Base address for this API client</param>
|
||||||
/// <param name="options">The base client options</param>
|
/// <param name="options">The base client options</param>
|
||||||
/// <param name="apiOptions">The Api client options</param>
|
/// <param name="apiOptions">The Api client options</param>
|
||||||
public RestApiClient(ILogger logger, HttpClient? httpClient, string baseAddress, RestExchangeOptions options, RestApiOptions apiOptions)
|
public RestApiClient(ILogger logger, HttpClient? httpClient, string baseAddress, RestExchangeOptions options, RestApiOptions apiOptions)
|
||||||
: base(logger,
|
: base(logger,
|
||||||
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
||||||
apiOptions.ApiCredentials ?? options.ApiCredentials,
|
apiOptions.ApiCredentials ?? options.ApiCredentials,
|
||||||
baseAddress,
|
baseAddress,
|
||||||
options,
|
options,
|
||||||
apiOptions)
|
apiOptions)
|
||||||
{
|
{
|
||||||
var rateLimiters = new List<IRateLimiter>();
|
|
||||||
foreach (var rateLimiter in apiOptions.RateLimiters)
|
|
||||||
rateLimiters.Add(rateLimiter);
|
|
||||||
RateLimiters = rateLimiters;
|
|
||||||
|
|
||||||
RequestFactory.Configure(options.Proxy, options.RequestTimeout, httpClient);
|
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>
|
||||||
|
/// Send a request to the base address based on the request definition
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="baseAddress">Host and schema</param>
|
||||||
|
/// <param name="definition">Request definition</param>
|
||||||
|
/// <param name="parameters">Request parameters</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <param name="additionalHeaders">Additional headers for this request</param>
|
||||||
|
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual async Task<WebCallResult> SendAsync(
|
||||||
|
string baseAddress,
|
||||||
|
RequestDefinition definition,
|
||||||
|
ParameterCollection? parameters,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
|
int? weight = null)
|
||||||
|
{
|
||||||
|
var result = await SendAsync<object>(baseAddress, definition, parameters, cancellationToken, additionalHeaders, weight).ConfigureAwait(false);
|
||||||
|
return result.AsDataless();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send a request to the base address based on the request definition
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Response type</typeparam>
|
||||||
|
/// <param name="baseAddress">Host and schema</param>
|
||||||
|
/// <param name="definition">Request definition</param>
|
||||||
|
/// <param name="parameters">Request parameters</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <param name="additionalHeaders">Additional headers for this request</param>
|
||||||
|
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual async Task<WebCallResult<T>> SendAsync<T>(
|
||||||
|
string baseAddress,
|
||||||
|
RequestDefinition definition,
|
||||||
|
ParameterCollection? parameters,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
|
int? weight = null) where T : class
|
||||||
|
{
|
||||||
|
int currentTry = 0;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
currentTry++;
|
||||||
|
var prepareResult = await PrepareAsync(baseAddress, definition, parameters, cancellationToken, additionalHeaders, weight).ConfigureAwait(false);
|
||||||
|
if (!prepareResult)
|
||||||
|
return new WebCallResult<T>(prepareResult.Error!);
|
||||||
|
|
||||||
|
var request = CreateRequest(baseAddress, definition, parameters, additionalHeaders);
|
||||||
|
_logger.RestApiSendRequest(request.RequestId, definition, request.Content, request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")));
|
||||||
|
TotalRequestsMade++;
|
||||||
|
var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (!result)
|
||||||
|
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString());
|
||||||
|
else
|
||||||
|
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]");
|
||||||
|
|
||||||
|
if (await ShouldRetryRequestAsync(definition.RateLimitGate, result, currentTry).ConfigureAwait(false))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepare before sending a request. Sync time between client and server and check rate limits
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="baseAddress">Host and schema</param>
|
||||||
|
/// <param name="definition">Request definition</param>
|
||||||
|
/// <param name="parameters">Request parameters</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <param name="additionalHeaders">Additional headers for this request</param>
|
||||||
|
/// <param name="weight">Override the request weight for this request</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
protected virtual async Task<CallResult> PrepareAsync(
|
||||||
|
string baseAddress,
|
||||||
|
RequestDefinition definition,
|
||||||
|
ParameterCollection? parameters,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
|
int? weight = null)
|
||||||
|
{
|
||||||
|
var requestId = ExchangeHelpers.NextId();
|
||||||
|
var requestWeight = weight ?? definition.Weight;
|
||||||
|
|
||||||
|
// Time sync
|
||||||
|
if (definition.Authenticated)
|
||||||
|
{
|
||||||
|
if (AuthenticationProvider == null)
|
||||||
|
{
|
||||||
|
_logger.RestApiNoApiCredentials(requestId, definition.Path);
|
||||||
|
return new CallResult<IRequest>(new NoApiCredentialsError());
|
||||||
|
}
|
||||||
|
|
||||||
|
var syncTask = SyncTimeAsync();
|
||||||
|
var timeSyncInfo = GetTimeSyncInfo();
|
||||||
|
|
||||||
|
if (timeSyncInfo != null && timeSyncInfo.TimeSyncState.LastSyncTime == default)
|
||||||
|
{
|
||||||
|
// Initially with first request we'll need to wait for the time syncing, if it's not the first request we can just continue
|
||||||
|
var syncTimeResult = await syncTask.ConfigureAwait(false);
|
||||||
|
if (!syncTimeResult)
|
||||||
|
{
|
||||||
|
_logger.RestApiFailedToSyncTime(requestId, syncTimeResult.Error!.ToString());
|
||||||
|
return syncTimeResult.AsDataless();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
if (requestWeight != 0)
|
||||||
|
{
|
||||||
|
if (definition.RateLimitGate == null)
|
||||||
|
throw new Exception("Ratelimit gate not set when request weight is not 0");
|
||||||
|
|
||||||
|
if (ClientOptions.RateLimiterEnabled)
|
||||||
|
{
|
||||||
|
var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, ApiOptions.ApiCredentials?.Key ?? ClientOptions.ApiCredentials?.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (!limitResult)
|
||||||
|
return new CallResult(limitResult.Error!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Endpoint specific rate limiting
|
||||||
|
if (definition.EndpointLimitCount != null && definition.EndpointLimitPeriod != null)
|
||||||
|
{
|
||||||
|
if (definition.RateLimitGate == null)
|
||||||
|
throw new Exception("Ratelimit gate not set when endpoint limit is specified");
|
||||||
|
|
||||||
|
if (ClientOptions.RateLimiterEnabled)
|
||||||
|
{
|
||||||
|
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, ApiOptions.ApiCredentials?.Key ?? ClientOptions.ApiCredentials?.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (!limitResult)
|
||||||
|
return new CallResult(limitResult.Error!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CallResult(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a request object
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="baseAddress">Host and schema</param>
|
||||||
|
/// <param name="definition">Request definition</param>
|
||||||
|
/// <param name="parameters">The parameters of the request</param>
|
||||||
|
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual IRequest CreateRequest(
|
||||||
|
string baseAddress,
|
||||||
|
RequestDefinition definition,
|
||||||
|
ParameterCollection? parameters,
|
||||||
|
Dictionary<string, string>? additionalHeaders)
|
||||||
|
{
|
||||||
|
parameters ??= new ParameterCollection();
|
||||||
|
var uri = new Uri(baseAddress.AppendPath(definition.Path));
|
||||||
|
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
|
||||||
|
var arraySerialization = definition.ArraySerialization ?? ArraySerialization;
|
||||||
|
var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat;
|
||||||
|
var requestId = ExchangeHelpers.NextId();
|
||||||
|
|
||||||
|
for (var i = 0; i < parameters.Count; i++)
|
||||||
|
{
|
||||||
|
var kvp = parameters.ElementAt(i);
|
||||||
|
if (kvp.Value is Func<object> delegateValue)
|
||||||
|
parameters[kvp.Key] = delegateValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterPosition == HttpMethodParameterPosition.InUri)
|
||||||
|
{
|
||||||
|
foreach (var parameter in parameters)
|
||||||
|
uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
var headers = new Dictionary<string, string>();
|
||||||
|
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
||||||
|
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
||||||
|
if (AuthenticationProvider != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AuthenticationProvider.AuthenticateRequest(
|
||||||
|
this,
|
||||||
|
uri,
|
||||||
|
definition.Method,
|
||||||
|
parameters,
|
||||||
|
definition.Authenticated,
|
||||||
|
arraySerialization,
|
||||||
|
parameterPosition,
|
||||||
|
bodyFormat,
|
||||||
|
out uriParameters,
|
||||||
|
out bodyParameters,
|
||||||
|
out headers);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity check
|
||||||
|
foreach (var param in parameters)
|
||||||
|
{
|
||||||
|
if (!uriParameters.ContainsKey(param.Key) && !bodyParameters.ContainsKey(param.Key))
|
||||||
|
{
|
||||||
|
throw new Exception($"Missing parameter {param.Key} after authentication processing. AuthenticationProvider implementation " +
|
||||||
|
$"should return provided parameters in either the uri or body parameters output");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
|
||||||
|
uri = uri.SetParameters(uriParameters, arraySerialization);
|
||||||
|
|
||||||
|
var request = RequestFactory.Create(definition.Method, uri, requestId);
|
||||||
|
request.Accept = Constants.JsonContentHeader;
|
||||||
|
|
||||||
|
foreach (var header in headers)
|
||||||
|
request.AddHeader(header.Key, header.Value);
|
||||||
|
|
||||||
|
if (additionalHeaders != null)
|
||||||
|
{
|
||||||
|
foreach (var header in additionalHeaders)
|
||||||
|
request.AddHeader(header.Key, header.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StandardRequestHeaders != null)
|
||||||
|
{
|
||||||
|
foreach (var header in StandardRequestHeaders)
|
||||||
|
{
|
||||||
|
// Only add it if it isn't overwritten
|
||||||
|
if (additionalHeaders?.ContainsKey(header.Key) != true)
|
||||||
|
request.AddHeader(header.Key, header.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterPosition == HttpMethodParameterPosition.InBody)
|
||||||
|
{
|
||||||
|
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
||||||
|
if (bodyParameters.Any())
|
||||||
|
WriteParamBody(request, bodyParameters, contentType);
|
||||||
|
else
|
||||||
|
request.SetContent(RequestBodyEmptyContent, contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Execute a request to the uri and returns if it was successful
|
/// Execute a request to the uri and returns if it was successful
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -87,9 +367,8 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
/// <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="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="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="additionalHeaders">Additional headers to send with the request</param>
|
||||||
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
|
/// <param name="gate">The ratelimit gate to use</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[return: NotNull]
|
[return: NotNull]
|
||||||
protected virtual async Task<WebCallResult> SendRequestAsync(
|
protected virtual async Task<WebCallResult> SendRequestAsync(
|
||||||
@@ -102,25 +381,24 @@ namespace CryptoExchange.Net
|
|||||||
HttpMethodParameterPosition? parameterPosition = null,
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
ArrayParametersSerialization? arraySerialization = null,
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
int requestWeight = 1,
|
int requestWeight = 1,
|
||||||
JsonSerializer? deserializer = null,
|
|
||||||
Dictionary<string, string>? additionalHeaders = null,
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
bool ignoreRatelimit = false)
|
IRateLimitGate? gate = null)
|
||||||
{
|
{
|
||||||
int currentTry = 0;
|
int currentTry = 0;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
currentTry++;
|
currentTry++;
|
||||||
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
|
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, additionalHeaders, gate).ConfigureAwait(false);
|
||||||
if (!request)
|
if (!request)
|
||||||
return new WebCallResult(request.Error!);
|
return new WebCallResult(request.Error!);
|
||||||
|
|
||||||
var result = await GetResponseAsync<object>(request.Data, deserializer, cancellationToken, true).ConfigureAwait(false);
|
var result = await GetResponseAsync<object>(request.Data, gate, cancellationToken).ConfigureAwait(false);
|
||||||
if (!result)
|
if (!result)
|
||||||
_logger.Log(LogLevel.Warning, $"[Req {result.RequestId}] Error received in {result.ResponseTime!.Value.TotalMilliseconds}ms: {result.Error}");
|
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString());
|
||||||
else
|
else
|
||||||
_logger.Log(LogLevel.Debug, $"[Req {result.RequestId}] Response received in {result.ResponseTime!.Value.TotalMilliseconds}ms{(OutputOriginalData ? (": " + result.OriginalData) : "")}");
|
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]");
|
||||||
|
|
||||||
if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false))
|
if (await ShouldRetryRequestAsync(gate, result, currentTry).ConfigureAwait(false))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
return result.AsDataless();
|
return result.AsDataless();
|
||||||
@@ -140,9 +418,8 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
/// <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="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="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="additionalHeaders">Additional headers to send with the request</param>
|
||||||
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
|
/// <param name="gate">The ratelimit gate to use</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[return: NotNull]
|
[return: NotNull]
|
||||||
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
|
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
|
||||||
@@ -155,26 +432,25 @@ namespace CryptoExchange.Net
|
|||||||
HttpMethodParameterPosition? parameterPosition = null,
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
ArrayParametersSerialization? arraySerialization = null,
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
int requestWeight = 1,
|
int requestWeight = 1,
|
||||||
JsonSerializer? deserializer = null,
|
|
||||||
Dictionary<string, string>? additionalHeaders = null,
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
bool ignoreRatelimit = false
|
IRateLimitGate? gate = null
|
||||||
) where T : class
|
) where T : class
|
||||||
{
|
{
|
||||||
int currentTry = 0;
|
int currentTry = 0;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
currentTry++;
|
currentTry++;
|
||||||
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
|
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, additionalHeaders, gate).ConfigureAwait(false);
|
||||||
if (!request)
|
if (!request)
|
||||||
return new WebCallResult<T>(request.Error!);
|
return new WebCallResult<T>(request.Error!);
|
||||||
|
|
||||||
var result = await GetResponseAsync<T>(request.Data, deserializer, cancellationToken, false).ConfigureAwait(false);
|
var result = await GetResponseAsync<T>(request.Data, gate, cancellationToken).ConfigureAwait(false);
|
||||||
if (!result)
|
if (!result)
|
||||||
_logger.Log(LogLevel.Warning, $"[Req {result.RequestId}] Error received in {result.ResponseTime!.Value.TotalMilliseconds}ms: {result.Error}");
|
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString());
|
||||||
else
|
else
|
||||||
_logger.Log(LogLevel.Debug, $"[Req {result.RequestId}] Response received in {result.ResponseTime!.Value.TotalMilliseconds}ms{(OutputOriginalData ? (": " + result.OriginalData) : "")}");
|
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]");
|
||||||
|
|
||||||
if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false))
|
if (await ShouldRetryRequestAsync(gate, result, currentTry).ConfigureAwait(false))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -193,9 +469,8 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
/// <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="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="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="additionalHeaders">Additional headers to send with the request</param>
|
||||||
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
|
/// <param name="gate">The rate limit gate to use</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult<IRequest>> PrepareRequestAsync(
|
protected virtual async Task<CallResult<IRequest>> PrepareRequestAsync(
|
||||||
Uri uri,
|
Uri uri,
|
||||||
@@ -207,14 +482,19 @@ namespace CryptoExchange.Net
|
|||||||
HttpMethodParameterPosition? parameterPosition = null,
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
ArrayParametersSerialization? arraySerialization = null,
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
int requestWeight = 1,
|
int requestWeight = 1,
|
||||||
JsonSerializer? deserializer = null,
|
|
||||||
Dictionary<string, string>? additionalHeaders = null,
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
bool ignoreRatelimit = false)
|
IRateLimitGate? gate = null)
|
||||||
{
|
{
|
||||||
var requestId = ExchangeHelpers.NextId();
|
var requestId = ExchangeHelpers.NextId();
|
||||||
|
|
||||||
if (signed)
|
if (signed)
|
||||||
{
|
{
|
||||||
|
if (AuthenticationProvider == null)
|
||||||
|
{
|
||||||
|
_logger.RestApiNoApiCredentials(requestId, uri.AbsolutePath);
|
||||||
|
return new CallResult<IRequest>(new NoApiCredentialsError());
|
||||||
|
}
|
||||||
|
|
||||||
var syncTask = SyncTimeAsync();
|
var syncTask = SyncTimeAsync();
|
||||||
var timeSyncInfo = GetTimeSyncInfo();
|
var timeSyncInfo = GetTimeSyncInfo();
|
||||||
|
|
||||||
@@ -224,31 +504,28 @@ namespace CryptoExchange.Net
|
|||||||
var syncTimeResult = await syncTask.ConfigureAwait(false);
|
var syncTimeResult = await syncTask.ConfigureAwait(false);
|
||||||
if (!syncTimeResult)
|
if (!syncTimeResult)
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Debug, $"[Req {requestId}] Failed to sync time, aborting request: " + syncTimeResult.Error);
|
_logger.RestApiFailedToSyncTime(requestId, syncTimeResult.Error!.ToString());
|
||||||
return syncTimeResult.As<IRequest>(default);
|
return syncTimeResult.As<IRequest>(default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ignoreRatelimit)
|
if (requestWeight != 0)
|
||||||
{
|
{
|
||||||
foreach (var limiter in RateLimiters)
|
if (gate == null)
|
||||||
|
throw new Exception("Ratelimit gate not set when request weight is not 0");
|
||||||
|
|
||||||
|
if (ClientOptions.RateLimiterEnabled)
|
||||||
{
|
{
|
||||||
var limitResult = await limiter.LimitRequestAsync(_logger, uri.AbsolutePath, method, signed, ApiOptions.ApiCredentials?.Key ?? ClientOptions.ApiCredentials?.Key, ApiOptions.RateLimitingBehaviour, requestWeight, cancellationToken).ConfigureAwait(false);
|
var limitResult = await gate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, new RequestDefinition(uri.AbsolutePath.TrimStart('/'), method) { Authenticated = signed }, uri.Host, ApiOptions.ApiCredentials?.Key ?? ClientOptions.ApiCredentials?.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
|
||||||
if (!limitResult.Success)
|
if (!limitResult)
|
||||||
return new CallResult<IRequest>(limitResult.Error!);
|
return new CallResult<IRequest>(limitResult.Error!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (signed && AuthenticationProvider == null)
|
_logger.RestApiCreatingRequest(requestId, uri);
|
||||||
{
|
|
||||||
_logger.Log(LogLevel.Warning, $"[Req {requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
|
|
||||||
return new CallResult<IRequest>(new NoApiCredentialsError());
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.Log(LogLevel.Information, $"[Req {requestId}] Creating request for " + uri);
|
|
||||||
var paramsPosition = parameterPosition ?? ParameterPositions[method];
|
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 ?? this.arraySerialization, requestBodyFormat ?? this.requestBodyFormat, requestId, additionalHeaders);
|
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);
|
||||||
|
|
||||||
string? paramString = "";
|
string? paramString = "";
|
||||||
if (paramsPosition == HttpMethodParameterPosition.InBody)
|
if (paramsPosition == HttpMethodParameterPosition.InBody)
|
||||||
@@ -259,7 +536,7 @@ namespace CryptoExchange.Net
|
|||||||
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
|
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
|
||||||
|
|
||||||
TotalRequestsMade++;
|
TotalRequestsMade++;
|
||||||
_logger.Log(LogLevel.Trace, $"[Req {requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}");
|
_logger.RestApiSendingRequest(requestId, method, signed ? "signed": "", request.Uri, paramString);
|
||||||
return new CallResult<IRequest>(request);
|
return new CallResult<IRequest>(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,109 +544,77 @@ namespace CryptoExchange.Net
|
|||||||
/// Executes the request and returns the result deserialized into the type parameter class
|
/// Executes the request and returns the result deserialized into the type parameter class
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">The request object to execute</param>
|
/// <param name="request">The request object to execute</param>
|
||||||
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
/// <param name="gate">The ratelimit gate used</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <param name="expectedEmptyResponse">If an empty response is expected</param>
|
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(
|
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(
|
||||||
IRequest request,
|
IRequest request,
|
||||||
JsonSerializer? deserializer,
|
IRateLimitGate? gate,
|
||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken)
|
||||||
bool expectedEmptyResponse)
|
|
||||||
{
|
{
|
||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
|
Stream? responseStream = null;
|
||||||
|
IResponse? response = null;
|
||||||
|
IStreamMessageAccessor? accessor = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
|
response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
|
||||||
sw.Stop();
|
sw.Stop();
|
||||||
var statusCode = response.StatusCode;
|
var statusCode = response.StatusCode;
|
||||||
var headers = response.ResponseHeaders;
|
var headers = response.ResponseHeaders;
|
||||||
var responseLength = response.ContentLength;
|
var responseLength = response.ContentLength;
|
||||||
var responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
|
responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
|
||||||
if (response.IsSuccessStatusCode)
|
var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData;
|
||||||
|
|
||||||
|
accessor = CreateAccessor();
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
// If we have to manually parse error responses (can't rely on HttpStatusCode) we'll need to read the full
|
// Error response
|
||||||
// response before being able to deserialize it into the resulting type since we don't know if its an error response or data
|
await accessor.Read(responseStream, true).ConfigureAwait(false);
|
||||||
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;
|
Error error;
|
||||||
if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429)
|
if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429)
|
||||||
error = ParseRateLimitResponse((int)response.StatusCode, response.ResponseHeaders, data);
|
{
|
||||||
|
var rateError = ParseRateLimitResponse((int)response.StatusCode, response.ResponseHeaders, accessor);
|
||||||
|
if (rateError.RetryAfter != null && gate != null && ClientOptions.RateLimiterEnabled)
|
||||||
|
{
|
||||||
|
_logger.RestApiRateLimitPauseUntil(request.RequestId, rateError.RetryAfter.Value);
|
||||||
|
await gate.SetRetryAfterGuardAsync(rateError.RetryAfter.Value).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
error = rateError;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, data);
|
{
|
||||||
|
error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor);
|
||||||
|
}
|
||||||
|
|
||||||
if (error.Code == null || error.Code == 0)
|
if (error.Code == null || error.Code == 0)
|
||||||
error.Code = (int)response.StatusCode;
|
error.Code = (int)response.StatusCode;
|
||||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data.Length, data, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
|
|
||||||
|
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!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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("Failed to parse response", 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)
|
catch (HttpRequestException requestException)
|
||||||
{
|
{
|
||||||
@@ -390,6 +635,12 @@ namespace CryptoExchange.Net
|
|||||||
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"));
|
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>
|
/// <summary>
|
||||||
@@ -397,22 +648,43 @@ namespace CryptoExchange.Net
|
|||||||
/// 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.
|
/// 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
|
/// If the response is an error this method should return the parsed error, else it should return null
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="data">Received data</param>
|
/// <param name="accessor">Data accessor</param>
|
||||||
/// <returns>Null if not an error, Error otherwise</returns>
|
/// <returns>Null if not an error, Error otherwise</returns>
|
||||||
protected virtual Task<ServerError?> TryParseErrorAsync(JToken data)
|
protected virtual ServerError? TryParseError(IMessageAccessor accessor) => null;
|
||||||
{
|
|
||||||
return Task.FromResult<ServerError?>(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <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.
|
/// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever.
|
||||||
/// Note that this is always called; even when the request might be successful
|
/// Note that this is always called; even when the request might be successful
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">WebCallResult type parameter</typeparam>
|
/// <typeparam name="T">WebCallResult type parameter</typeparam>
|
||||||
|
/// <param name="gate">The rate limit gate the call used</param>
|
||||||
/// <param name="callResult">The result of the call</param>
|
/// <param name="callResult">The result of the call</param>
|
||||||
/// <param name="tries">The current try number</param>
|
/// <param name="tries">The current try number</param>
|
||||||
/// <returns>True if call should retry, false if the call should return</returns>
|
/// <returns>True if call should retry, false if the call should return</returns>
|
||||||
protected virtual Task<bool> ShouldRetryRequestAsync<T>(WebCallResult<T> callResult, int tries) => Task.FromResult(false);
|
protected virtual async Task<bool> ShouldRetryRequestAsync<T>(IRateLimitGate? gate, WebCallResult<T> callResult, int tries)
|
||||||
|
{
|
||||||
|
if (tries >= 2)
|
||||||
|
// Only retry once
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if ((int?)callResult.ResponseStatusCode == 429
|
||||||
|
&& ClientOptions.RateLimiterEnabled
|
||||||
|
&& ClientOptions.RateLimitingBehaviour != RateLimitingBehaviour.Fail
|
||||||
|
&& gate != null)
|
||||||
|
{
|
||||||
|
var retryTime = await gate.GetRetryAfterTime().ConfigureAwait(false);
|
||||||
|
if (retryTime == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (retryTime.Value - DateTime.UtcNow < TimeSpan.FromSeconds(60))
|
||||||
|
{
|
||||||
|
_logger.RestApiRateLimitRetry(callResult.RequestId!.Value, retryTime.Value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a request object
|
/// Creates a request object
|
||||||
@@ -468,6 +740,7 @@ namespace CryptoExchange.Net
|
|||||||
signed,
|
signed,
|
||||||
arraySerialization,
|
arraySerialization,
|
||||||
parameterPosition,
|
parameterPosition,
|
||||||
|
bodyFormat,
|
||||||
out uriParameters,
|
out uriParameters,
|
||||||
out bodyParameters,
|
out bodyParameters,
|
||||||
out headers);
|
out headers);
|
||||||
@@ -519,7 +792,7 @@ namespace CryptoExchange.Net
|
|||||||
if (bodyParameters.Any())
|
if (bodyParameters.Any())
|
||||||
WriteParamBody(request, bodyParameters, contentType);
|
WriteParamBody(request, bodyParameters, contentType);
|
||||||
else
|
else
|
||||||
request.SetContent(requestBodyEmptyContent, contentType);
|
request.SetContent(RequestBodyEmptyContent, contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
return request;
|
return request;
|
||||||
@@ -536,7 +809,7 @@ namespace CryptoExchange.Net
|
|||||||
if (contentType == Constants.JsonContentHeader)
|
if (contentType == Constants.JsonContentHeader)
|
||||||
{
|
{
|
||||||
// Write the parameters as json in the body
|
// Write the parameters as json in the body
|
||||||
var stringData = JsonConvert.SerializeObject(parameters);
|
var stringData = CreateSerializer().Serialize(parameters);
|
||||||
request.SetContent(stringData, contentType);
|
request.SetContent(stringData, contentType);
|
||||||
}
|
}
|
||||||
else if (contentType == Constants.FormContentHeader)
|
else if (contentType == Constants.FormContentHeader)
|
||||||
@@ -552,35 +825,38 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="httpStatusCode">The response status code</param>
|
/// <param name="httpStatusCode">The response status code</param>
|
||||||
/// <param name="responseHeaders">The response headers</param>
|
/// <param name="responseHeaders">The response headers</param>
|
||||||
/// <param name="data">The response data</param>
|
/// <param name="accessor">Data accessor</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, string data)
|
protected virtual Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, IMessageAccessor accessor)
|
||||||
{
|
{
|
||||||
return new ServerError(data);
|
var message = accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Error response content only available when OutputOriginal = true in client options]";
|
||||||
|
return new ServerError(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse a rate limit error response from the server. Only used when server returns http status 429 or 418
|
/// Parse a rate limit error response from the server. Only used when server returns http status 429 or 418
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="httpStatusCode">The response status code</param>
|
/// <param name="httpStatusCode">The response status code</param>
|
||||||
/// <param name="responseHeaders">The response headers</param>
|
/// <param name="responseHeaders">The response headers</param>
|
||||||
/// <param name="data">The response data</param>
|
/// <param name="accessor">Data accessor</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual Error ParseRateLimitResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, string data)
|
protected virtual ServerRateLimitError ParseRateLimitResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, IMessageAccessor accessor)
|
||||||
{
|
{
|
||||||
|
var message = accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Error response content only available when OutputOriginal = true in client options]";
|
||||||
|
|
||||||
// Handle retry after header
|
// Handle retry after header
|
||||||
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
|
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
|
||||||
if (retryAfterHeader.Value?.Any() != true)
|
if (retryAfterHeader.Value?.Any() != true)
|
||||||
return new ServerRateLimitError(data);
|
return new ServerRateLimitError(message);
|
||||||
|
|
||||||
var value = retryAfterHeader.Value.First();
|
var value = retryAfterHeader.Value.First();
|
||||||
if (int.TryParse(value, out var seconds))
|
if (int.TryParse(value, out var seconds))
|
||||||
return new ServerRateLimitError(data) { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) };
|
return new ServerRateLimitError(message) { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) };
|
||||||
|
|
||||||
if (DateTime.TryParse(value, out var datetime))
|
if (DateTime.TryParse(value, out var datetime))
|
||||||
return new ServerRateLimitError(data) { RetryAfter = datetime };
|
return new ServerRateLimitError(message) { RetryAfter = datetime };
|
||||||
|
|
||||||
return new ServerRateLimitError(data);
|
return new ServerRateLimitError(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -597,7 +873,7 @@ namespace CryptoExchange.Net
|
|||||||
|
|
||||||
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
|
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();
|
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
|
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
|
||||||
@@ -624,7 +900,7 @@ namespace CryptoExchange.Net
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate time offset between local and server
|
// 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.UpdateTimeOffset(offset);
|
||||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
|
using CryptoExchange.Net.Converters.JsonNet;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Logging.Extensions;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Sockets;
|
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net.Clients
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base socket API client for interaction with a websocket API
|
/// Base socket API client for interaction with a websocket API
|
||||||
@@ -61,10 +60,15 @@ namespace CryptoExchange.Net
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The rate limiters
|
/// The rate limiters
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected internal IEnumerable<IRateLimiter>? RateLimiters { get; set; }
|
protected internal IRateLimitGate? RateLimiter { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Periodic task regisrations
|
/// The max size a websocket message size can be
|
||||||
|
/// </summary>
|
||||||
|
protected internal int? MessageSendSizeLimit { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Periodic task registrations
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected List<PeriodicTaskRegistration> PeriodicTaskRegistrations { get; set; } = new List<PeriodicTaskRegistration>();
|
protected List<PeriodicTaskRegistration> PeriodicTaskRegistrations { get; set; } = new List<PeriodicTaskRegistration>();
|
||||||
|
|
||||||
@@ -110,20 +114,28 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="options">Client options</param>
|
/// <param name="options">Client options</param>
|
||||||
/// <param name="baseAddress">Base address for this API client</param>
|
/// <param name="baseAddress">Base address for this API client</param>
|
||||||
/// <param name="apiOptions">The Api client options</param>
|
/// <param name="apiOptions">The Api client options</param>
|
||||||
public SocketApiClient(ILogger logger, string baseAddress, SocketExchangeOptions options, SocketApiOptions apiOptions)
|
public SocketApiClient(ILogger logger, string baseAddress, SocketExchangeOptions options, SocketApiOptions apiOptions)
|
||||||
: base(logger,
|
: base(logger,
|
||||||
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
||||||
apiOptions.ApiCredentials ?? options.ApiCredentials,
|
apiOptions.ApiCredentials ?? options.ApiCredentials,
|
||||||
baseAddress,
|
baseAddress,
|
||||||
options,
|
options,
|
||||||
apiOptions)
|
apiOptions)
|
||||||
{
|
{
|
||||||
var rateLimiters = new List<IRateLimiter>();
|
|
||||||
foreach (var rateLimiter in apiOptions.RateLimiters)
|
|
||||||
rateLimiters.Add(rateLimiter);
|
|
||||||
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>
|
/// <summary>
|
||||||
/// Add a query to periodically send on each connection
|
/// Add a query to periodically send on each connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -191,12 +203,13 @@ namespace CryptoExchange.Net
|
|||||||
return socketResult.As<UpdateSubscription>(null);
|
return socketResult.As<UpdateSubscription>(null);
|
||||||
|
|
||||||
socketConnection = socketResult.Data;
|
socketConnection = socketResult.Data;
|
||||||
|
subscription.HandleUpdatesBeforeConfirmation = subscription.HandleUpdatesBeforeConfirmation || HandleMessageBeforeConfirmation;
|
||||||
|
|
||||||
// Add a subscription on the socket connection
|
// Add a subscription on the socket connection
|
||||||
var success = socketConnection.CanAddSubscription();
|
var success = socketConnection.AddSubscription(subscription);
|
||||||
if (!success)
|
if (!success)
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Trace, $"[Sckt {socketConnection.SocketId}] failed to add subscription, retrying on different connection");
|
_logger.FailedToAddSubscriptionRetryOnDifferentConnection(socketConnection.SocketId);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,27 +237,23 @@ namespace CryptoExchange.Net
|
|||||||
|
|
||||||
if (socketConnection.PausedActivity)
|
if (socketConnection.PausedActivity)
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Warning, $"[Sckt {socketConnection.SocketId}] has been paused, can't subscribe at this moment");
|
_logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId);
|
||||||
return new CallResult<UpdateSubscription>(new ServerError("Socket is paused"));
|
return new CallResult<UpdateSubscription>(new ServerError("Socket is paused"));
|
||||||
}
|
}
|
||||||
|
|
||||||
var waitEvent = new AsyncResetEvent(false);
|
var waitEvent = new ManualResetEvent(false);
|
||||||
var subQuery = subscription.GetSubQuery(socketConnection);
|
var subQuery = subscription.GetSubQuery(socketConnection);
|
||||||
if (subQuery != null)
|
if (subQuery != null)
|
||||||
{
|
{
|
||||||
if (HandleMessageBeforeConfirmation)
|
|
||||||
socketConnection.AddSubscription(subscription);
|
|
||||||
|
|
||||||
// Send the request and wait for answer
|
// Send the request and wait for answer
|
||||||
var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, waitEvent).ConfigureAwait(false);
|
var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, waitEvent).ConfigureAwait(false);
|
||||||
if (!subResult)
|
if (!subResult)
|
||||||
{
|
{
|
||||||
waitEvent?.Set();
|
waitEvent?.Set();
|
||||||
_logger.Log(LogLevel.Warning, $"[Sckt {socketConnection.SocketId}] failed to subscribe: {subResult.Error}");
|
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
|
||||||
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
|
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
|
||||||
var unsubscribe = subResult.Error is CancellationRequestedError;
|
var unsubscribe = subResult.Error is CancellationRequestedError;
|
||||||
await socketConnection.CloseAsync(subscription, unsubscribe).ConfigureAwait(false);
|
await socketConnection.CloseAsync(subscription, unsubscribe).ConfigureAwait(false);
|
||||||
|
|
||||||
return new CallResult<UpdateSubscription>(subResult.Error!);
|
return new CallResult<UpdateSubscription>(subResult.Error!);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,16 +265,13 @@ namespace CryptoExchange.Net
|
|||||||
{
|
{
|
||||||
subscription.CancellationTokenRegistration = ct.Register(async () =>
|
subscription.CancellationTokenRegistration = ct.Register(async () =>
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Information, $"[Sckt {socketConnection.SocketId}] Cancellation token set, closing subscription {subscription.Id}");
|
_logger.CancellationTokenSetClosingSubscription(socketConnection.SocketId, subscription.Id);
|
||||||
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
||||||
}, false);
|
}, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!HandleMessageBeforeConfirmation)
|
|
||||||
socketConnection.AddSubscription(subscription);
|
|
||||||
|
|
||||||
waitEvent?.Set();
|
waitEvent?.Set();
|
||||||
_logger.Log(LogLevel.Information, $"[Sckt {socketConnection.SocketId}] subscription {subscription.Id} completed successfully");
|
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
|
||||||
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
|
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,7 +328,7 @@ namespace CryptoExchange.Net
|
|||||||
|
|
||||||
if (socketConnection.PausedActivity)
|
if (socketConnection.PausedActivity)
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Warning, $"[Sckt {socketConnection.SocketId}] has been paused, can't send query at this moment");
|
_logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId);
|
||||||
return new CallResult<T>(new ServerError("Socket is paused"));
|
return new CallResult<T>(new ServerError("Socket is paused"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,20 +341,20 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="socket">The connection to check</param>
|
/// <param name="socket">The connection to check</param>
|
||||||
/// <param name="authenticated">Whether the socket should authenticated</param>
|
/// <param name="authenticated">Whether the socket should authenticated</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult<bool>> ConnectIfNeededAsync(SocketConnection socket, bool authenticated)
|
protected virtual async Task<CallResult> ConnectIfNeededAsync(SocketConnection socket, bool authenticated)
|
||||||
{
|
{
|
||||||
if (socket.Connected)
|
if (socket.Connected)
|
||||||
return new CallResult<bool>(true);
|
return new CallResult(null);
|
||||||
|
|
||||||
var connectResult = await ConnectSocketAsync(socket).ConfigureAwait(false);
|
var connectResult = await ConnectSocketAsync(socket).ConfigureAwait(false);
|
||||||
if (!connectResult)
|
if (!connectResult)
|
||||||
return new CallResult<bool>(connectResult.Error!);
|
return connectResult;
|
||||||
|
|
||||||
if (ClientOptions.DelayAfterConnect != TimeSpan.Zero)
|
if (ClientOptions.DelayAfterConnect != TimeSpan.Zero)
|
||||||
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
|
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!authenticated || socket.Authenticated)
|
if (!authenticated || socket.Authenticated)
|
||||||
return new CallResult<bool>(true);
|
return new CallResult(null);
|
||||||
|
|
||||||
return await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
return await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -358,12 +364,12 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="socket">Socket to authenticate</param>
|
/// <param name="socket">Socket to authenticate</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async Task<CallResult<bool>> AuthenticateSocketAsync(SocketConnection socket)
|
public virtual async Task<CallResult> AuthenticateSocketAsync(SocketConnection socket)
|
||||||
{
|
{
|
||||||
if (AuthenticationProvider == null)
|
if (AuthenticationProvider == null)
|
||||||
return new CallResult<bool>(new NoApiCredentialsError());
|
return new CallResult(new NoApiCredentialsError());
|
||||||
|
|
||||||
_logger.Log(LogLevel.Debug, $"[Sckt {socket.SocketId}] Attempting to authenticate");
|
_logger.AttemptingToAuthenticate(socket.SocketId);
|
||||||
var authRequest = GetAuthenticationRequest();
|
var authRequest = GetAuthenticationRequest();
|
||||||
if (authRequest != null)
|
if (authRequest != null)
|
||||||
{
|
{
|
||||||
@@ -371,18 +377,18 @@ namespace CryptoExchange.Net
|
|||||||
|
|
||||||
if (!result)
|
if (!result)
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Warning, $"[Sckt {socket.SocketId}] authentication failed");
|
_logger.AuthenticationFailed(socket.SocketId);
|
||||||
if (socket.Connected)
|
if (socket.Connected)
|
||||||
await socket.CloseAsync().ConfigureAwait(false);
|
await socket.CloseAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
result.Error!.Message = "Authentication failed: " + result.Error.Message;
|
result.Error!.Message = "Authentication failed: " + result.Error.Message;
|
||||||
return new CallResult<bool>(result.Error)!;
|
return new CallResult(result.Error)!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.Log(LogLevel.Debug, $"[Sckt {socket.SocketId}] authenticated");
|
_logger.Authenticated(socket.SocketId);
|
||||||
socket.Authenticated = true;
|
socket.Authenticated = true;
|
||||||
return new CallResult<bool>(true);
|
return new CallResult(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -424,13 +430,13 @@ namespace CryptoExchange.Net
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update the original request to send when the connection is restored after disconnecting. Can be used to update an authentication token for example.
|
/// Update the subscription when the connection is restored after disconnecting. Can be used to update an authentication token for example.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">The original request</param>
|
/// <param name="subscription">The subscription</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected internal virtual Task<CallResult<object>> RevitalizeRequestAsync(object request)
|
protected internal virtual Task<CallResult> RevitalizeRequestAsync(Subscription subscription)
|
||||||
{
|
{
|
||||||
return Task.FromResult(new CallResult<object>(request));
|
return Task.FromResult(new CallResult(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -443,27 +449,25 @@ namespace CryptoExchange.Net
|
|||||||
{
|
{
|
||||||
var socketResult = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
|
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.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();
|
&& (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;
|
var result = socketResult.Equals(default(KeyValuePair<int, SocketConnection>)) ? null : socketResult.Value;
|
||||||
if (result != null)
|
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
|
// 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);
|
return new CallResult<SocketConnection>(result);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
||||||
if (!connectionAddress)
|
if (!connectionAddress)
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Warning, $"Failed to determine connection url: " + connectionAddress.Error);
|
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error?.ToString());
|
||||||
return connectionAddress.As<SocketConnection>(null);
|
return connectionAddress.As<SocketConnection>(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connectionAddress.Data != address)
|
if (connectionAddress.Data != address)
|
||||||
_logger.Log(LogLevel.Debug, $"Connection address set to " + connectionAddress.Data);
|
_logger.ConnectionAddressSetTo(connectionAddress.Data!);
|
||||||
|
|
||||||
// Create new socket
|
// Create new socket
|
||||||
var socket = CreateSocket(connectionAddress.Data!);
|
var socket = CreateSocket(connectionAddress.Data!);
|
||||||
@@ -492,16 +496,17 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="socketConnection">The socket to connect</param>
|
/// <param name="socketConnection">The socket to connect</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult<bool>> ConnectSocketAsync(SocketConnection socketConnection)
|
protected virtual async Task<CallResult> ConnectSocketAsync(SocketConnection socketConnection)
|
||||||
{
|
{
|
||||||
if (await socketConnection.ConnectAsync().ConfigureAwait(false))
|
var connectResult = await socketConnection.ConnectAsync().ConfigureAwait(false);
|
||||||
|
if (connectResult)
|
||||||
{
|
{
|
||||||
socketConnections.TryAdd(socketConnection.SocketId, socketConnection);
|
socketConnections.TryAdd(socketConnection.SocketId, socketConnection);
|
||||||
return new CallResult<bool>(true);
|
return connectResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
socketConnection.Dispose();
|
socketConnection.Dispose();
|
||||||
return new CallResult<bool>(new CantConnectError());
|
return connectResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -514,7 +519,8 @@ namespace CryptoExchange.Net
|
|||||||
{
|
{
|
||||||
KeepAliveInterval = KeepAliveInterval,
|
KeepAliveInterval = KeepAliveInterval,
|
||||||
ReconnectInterval = ClientOptions.ReconnectInterval,
|
ReconnectInterval = ClientOptions.ReconnectInterval,
|
||||||
RateLimiters = RateLimiters,
|
RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null,
|
||||||
|
RateLimitingBehaviour = ClientOptions.RateLimitingBehaviour,
|
||||||
Proxy = ClientOptions.Proxy,
|
Proxy = ClientOptions.Proxy,
|
||||||
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout
|
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout
|
||||||
};
|
};
|
||||||
@@ -527,7 +533,7 @@ namespace CryptoExchange.Net
|
|||||||
protected virtual IWebsocket CreateSocket(string address)
|
protected virtual IWebsocket CreateSocket(string address)
|
||||||
{
|
{
|
||||||
var socket = SocketFactory.CreateWebsocket(_logger, GetWebSocketParameters(address));
|
var socket = SocketFactory.CreateWebsocket(_logger, GetWebSocketParameters(address));
|
||||||
_logger.Log(LogLevel.Debug, $"[Sckt {socket.Id}] created for " + address);
|
_logger.SocketCreatedForAddress(socket.Id, address);
|
||||||
return socket;
|
return socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,7 +559,7 @@ namespace CryptoExchange.Net
|
|||||||
if (subscription == null || connection == null)
|
if (subscription == null || connection == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
_logger.Log(LogLevel.Information, $"[Sckt {connection.SocketId}] unsubscribing subscription " + subscriptionId);
|
_logger.UnsubscribingSubscription(connection.SocketId, subscriptionId);
|
||||||
await connection.CloseAsync(subscription).ConfigureAwait(false);
|
await connection.CloseAsync(subscription).ConfigureAwait(false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -568,7 +574,7 @@ namespace CryptoExchange.Net
|
|||||||
if (subscription == null)
|
if (subscription == null)
|
||||||
throw new ArgumentNullException(nameof(subscription));
|
throw new ArgumentNullException(nameof(subscription));
|
||||||
|
|
||||||
_logger.Log(LogLevel.Information, $"[Sckt {subscription.SocketId}] Unsubscribing subscription " + subscription.Id);
|
_logger.UnsubscribingSubscription(subscription.SocketId, subscription.Id);
|
||||||
await subscription.CloseAsync().ConfigureAwait(false);
|
await subscription.CloseAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,12 +588,12 @@ namespace CryptoExchange.Net
|
|||||||
if (sum == 0)
|
if (sum == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_logger.Log(LogLevel.Information, $"Unsubscribing all {socketConnections.Sum(s => s.Value.UserSubscriptionCount)} subscriptions");
|
_logger.UnsubscribingAll(socketConnections.Sum(s => s.Value.UserSubscriptionCount));
|
||||||
var tasks = new List<Task>();
|
var tasks = new List<Task>();
|
||||||
{
|
{
|
||||||
var socketList = socketConnections.Values;
|
var socketList = socketConnections.Values;
|
||||||
foreach (var sub in socketList)
|
foreach (var sub in socketList)
|
||||||
tasks.Add(sub.CloseAsync());
|
tasks.Add(sub.CloseAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||||
@@ -599,7 +605,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async Task ReconnectAsync()
|
public virtual async Task ReconnectAsync()
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Information, $"Reconnecting all {socketConnections.Count} connections");
|
_logger.ReconnectingAllConnections(socketConnections.Count);
|
||||||
var tasks = new List<Task>();
|
var tasks = new List<Task>();
|
||||||
{
|
{
|
||||||
var socketList = socketConnections.Values;
|
var socketList = socketConnections.Values;
|
||||||
@@ -613,31 +619,78 @@ namespace CryptoExchange.Net
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Log the current state of connections and subscriptions
|
/// Log the current state of connections and subscriptions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string GetSubscriptionsState()
|
public string GetSubscriptionsState(bool includeSubDetails = true)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
return GetState(includeSubDetails).ToString();
|
||||||
sb.AppendLine($"{GetType().Name}");
|
}
|
||||||
sb.AppendLine($" Connections: {socketConnections.Count}");
|
|
||||||
sb.AppendLine($" Subscriptions: {CurrentSubscriptions}");
|
/// <summary>
|
||||||
sb.AppendLine($" Download speed: {IncomingKbps} kbps");
|
/// Gets the state of the client
|
||||||
foreach (var connection in socketConnections)
|
/// </summary>
|
||||||
|
/// <param name="includeSubDetails">True to get details for each subscription</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public SocketApiClientState GetState(bool includeSubDetails = true)
|
||||||
|
{
|
||||||
|
var connectionStates = new List<SocketConnection.SocketConnectionState>();
|
||||||
|
foreach (var socketIdAndConnection in socketConnections)
|
||||||
{
|
{
|
||||||
sb.AppendLine($" Id: {connection.Key}");
|
SocketConnection connection = socketIdAndConnection.Value;
|
||||||
sb.AppendLine($" Address: {connection.Value.ConnectionUri}");
|
SocketConnection.SocketConnectionState connectionState = connection.GetState(includeSubDetails);
|
||||||
sb.AppendLine($" Subscriptions: {connection.Value.UserSubscriptionCount}");
|
connectionStates.Add(connectionState);
|
||||||
sb.AppendLine($" Status: {connection.Value.Status}");
|
}
|
||||||
sb.AppendLine($" Authenticated: {connection.Value.Authenticated}");
|
|
||||||
sb.AppendLine($" Download speed: {connection.Value.IncomingKbps} kbps");
|
return new SocketApiClientState(socketConnections.Count, CurrentSubscriptions, IncomingKbps, connectionStates);
|
||||||
sb.AppendLine($" Subscriptions:");
|
}
|
||||||
foreach (var subscription in connection.Value.Subscriptions)
|
|
||||||
{
|
/// <summary>
|
||||||
sb.AppendLine($" Id: {subscription.Id}");
|
/// Get the current state of the client
|
||||||
sb.AppendLine($" Confirmed: {subscription.Confirmed}");
|
/// </summary>
|
||||||
sb.AppendLine($" Invocations: {subscription.TotalInvocations}");
|
/// <param name="Connections">Number of sockets for this client</param>
|
||||||
sb.AppendLine($" Identifiers: [{string.Join(", ", subscription.ListenerIdentifiers)}]");
|
/// <param name="Subscriptions">Total number of subscriptions</param>
|
||||||
}
|
/// <param name="DownloadSpeed">Total download speed</param>
|
||||||
|
/// <param name="ConnectionStates">State of each socket connection</param>
|
||||||
|
public record SocketApiClientState(
|
||||||
|
int Connections,
|
||||||
|
int Subscriptions,
|
||||||
|
double DownloadSpeed,
|
||||||
|
List<SocketConnection.SocketConnectionState> ConnectionStates)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Print the state of the client
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sb"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual bool PrintMembers(StringBuilder sb)
|
||||||
|
{
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"\tTotal connections: {Connections}");
|
||||||
|
sb.AppendLine($"\tTotal subscriptions: {Subscriptions}");
|
||||||
|
sb.AppendLine($"\tDownload speed: {DownloadSpeed} kbps");
|
||||||
|
sb.AppendLine($"\tConnections:");
|
||||||
|
ConnectionStates.ForEach(cs =>
|
||||||
|
{
|
||||||
|
sb.AppendLine($"\t\tId: {cs.Id}");
|
||||||
|
sb.AppendLine($"\t\tAddress: {cs.Address}");
|
||||||
|
sb.AppendLine($"\t\tTotal subscriptions: {cs.Subscriptions}");
|
||||||
|
sb.AppendLine($"\t\tStatus: {cs.Status}");
|
||||||
|
sb.AppendLine($"\t\tAuthenticated: {cs.Authenticated}");
|
||||||
|
sb.AppendLine($"\t\tDownload speed: {cs.DownloadSpeed} kbps");
|
||||||
|
sb.AppendLine($"\t\tPending queries: {cs.PendingQueries}");
|
||||||
|
if (cs.SubscriptionStates?.Count > 0)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"\t\tSubscriptions:");
|
||||||
|
cs.SubscriptionStates.ForEach(subState =>
|
||||||
|
{
|
||||||
|
sb.AppendLine($"\t\t\tId: {subState.Id}");
|
||||||
|
sb.AppendLine($"\t\t\tConfirmed: {subState.Confirmed}");
|
||||||
|
sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}");
|
||||||
|
sb.AppendLine($"\t\t\tIdentifiers: [{string.Join(",", subState.Identifiers)}]");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return sb.ToString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -648,7 +701,7 @@ namespace CryptoExchange.Net
|
|||||||
_disposing = true;
|
_disposing = true;
|
||||||
if (socketConnections.Sum(s => s.Value.UserSubscriptionCount) > 0)
|
if (socketConnections.Sum(s => s.Value.UserSubscriptionCount) > 0)
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Debug, "Disposing socket client, closing all subscriptions");
|
_logger.DisposingSocketClient();
|
||||||
_ = UnsubscribeAllAsync();
|
_ = UnsubscribeAllAsync();
|
||||||
}
|
}
|
||||||
semaphoreSlim?.Dispose();
|
semaphoreSlim?.Dispose();
|
||||||
@@ -666,8 +719,8 @@ namespace CryptoExchange.Net
|
|||||||
/// Preprocess a stream message
|
/// Preprocess a stream message
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="type"></param>
|
/// <param name="type"></param>
|
||||||
/// <param name="stream"></param>
|
/// <param name="data"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual Stream PreprocessStreamMessage(WebSocketMessageType type, Stream stream) => stream;
|
public virtual ReadOnlyMemory<byte> PreprocessStreamMessage(WebSocketMessageType type, ReadOnlyMemory<byte> data) => data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-22
@@ -8,7 +8,7 @@ using CryptoExchange.Net.Attributes;
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters
|
namespace CryptoExchange.Net.Converters.JsonNet
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <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
|
/// 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,25 +192,4 @@ namespace CryptoExchange.Net.Converters
|
|||||||
private static T? GetCustomAttribute<T>(Type type) where T : Attribute =>
|
private static T? GetCustomAttribute<T>(Type type) where T : Attribute =>
|
||||||
(T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T)));
|
(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
-1
@@ -4,7 +4,7 @@ using System.Diagnostics;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters
|
namespace CryptoExchange.Net.Converters.JsonNet
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base class for enum converters
|
/// Base class for enum converters
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters
|
namespace CryptoExchange.Net.Converters.JsonNet
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Boolean converter with support for "0"/"1" (strings)
|
/// Boolean converter with support for "0"/"1" (strings)
|
||||||
+21
-4
@@ -4,7 +4,7 @@ using System.Diagnostics;
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters
|
namespace CryptoExchange.Net.Converters.JsonNet
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Datetime converter. Supports converting from string/long/double to DateTime and back. Numbers are assumed to be the time since 1970-01-01.
|
/// Datetime converter. Supports converting from string/long/double to DateTime and back. Numbers are assumed to be the time since 1970-01-01.
|
||||||
@@ -26,7 +26,12 @@ namespace CryptoExchange.Net.Converters
|
|||||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||||
{
|
{
|
||||||
if (reader.Value == null)
|
if (reader.Value == null)
|
||||||
|
{
|
||||||
|
if (objectType == typeof(DateTime))
|
||||||
|
return default(DateTime);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if(reader.TokenType is JsonToken.Integer)
|
if(reader.TokenType is JsonToken.Integer)
|
||||||
{
|
{
|
||||||
@@ -56,9 +61,6 @@ namespace CryptoExchange.Net.Converters
|
|||||||
else if(reader.TokenType is JsonToken.String)
|
else if(reader.TokenType is JsonToken.String)
|
||||||
{
|
{
|
||||||
var stringValue = (string)reader.Value;
|
var stringValue = (string)reader.Value;
|
||||||
if (string.IsNullOrWhiteSpace(stringValue))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(stringValue)
|
if (string.IsNullOrWhiteSpace(stringValue)
|
||||||
|| stringValue == "-1"
|
|| stringValue == "-1"
|
||||||
|| (double.TryParse(stringValue, out var doubleVal) && doubleVal == 0))
|
|| (double.TryParse(stringValue, out var doubleVal) && doubleVal == 0))
|
||||||
@@ -66,6 +68,21 @@ namespace CryptoExchange.Net.Converters
|
|||||||
return objectType == typeof(DateTime) ? default(DateTime) : null;
|
return objectType == typeof(DateTime) ? default(DateTime) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (stringValue.Length == 12 && stringValue.StartsWith("202"))
|
||||||
|
{
|
||||||
|
// Parse 202303261200 format
|
||||||
|
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
||||||
|
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
||||||
|
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|
||||||
|
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|
||||||
|
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
|
||||||
|
{
|
||||||
|
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
if (stringValue.Length == 8)
|
if (stringValue.Length == 8)
|
||||||
{
|
{
|
||||||
// Parse 20211103 format
|
// Parse 20211103 format
|
||||||
+2
-2
@@ -2,7 +2,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace Kraken.Net.Converters
|
namespace CryptoExchange.Net.Converters.JsonNet
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converter for serializing decimal values as string
|
/// Converter for serializing decimal values as string
|
||||||
@@ -14,7 +14,7 @@ namespace Kraken.Net.Converters
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool CanConvert(Type objectType) => objectType == typeof(decimal) || objectType == typeof(decimal?);
|
public override bool CanConvert(Type objectType) => objectType == typeof(decimal) || objectType == typeof(decimal?);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||||
{
|
{
|
||||||
+1
-1
@@ -7,7 +7,7 @@ using System.Diagnostics;
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters
|
namespace CryptoExchange.Net.Converters.JsonNet
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
|
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
|
||||||
@@ -0,0 +1,337 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-3
@@ -1,10 +1,10 @@
|
|||||||
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Sockets.MessageParsing
|
namespace CryptoExchange.Net.Converters.JsonNet
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public class JsonNetSerializer : IMessageSerializer
|
public class JsonNetMessageSerializer : IMessageSerializer
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Serialize(object message) => JsonConvert.SerializeObject(message, Formatting.None);
|
public string Serialize(object message) => JsonConvert.SerializeObject(message, Formatting.None);
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters
|
namespace CryptoExchange.Net.Converters.JsonNet
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Serializer options
|
/// Serializer options
|
||||||
+14
-9
@@ -1,4 +1,4 @@
|
|||||||
namespace CryptoExchange.Net.Sockets.MessageParsing
|
namespace CryptoExchange.Net.Converters.MessageParsing
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Node accessor
|
/// Node accessor
|
||||||
@@ -6,17 +6,23 @@
|
|||||||
public struct NodeAccessor
|
public struct NodeAccessor
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Value
|
/// Index
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public object? Value { get; }
|
public int? Index { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Property name
|
||||||
|
/// </summary>
|
||||||
|
public string? Property { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Type (0 = int, 1 = string, 2 = prop name)
|
/// Type (0 = int, 1 = string, 2 = prop name)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Type { get; }
|
public int Type { get; }
|
||||||
|
|
||||||
private NodeAccessor(object? value, int type)
|
private NodeAccessor(int? index, string? property, int type)
|
||||||
{
|
{
|
||||||
Value = value;
|
Index = index;
|
||||||
|
Property = property;
|
||||||
Type = type;
|
Type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,20 +31,19 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="value"></param>
|
/// <param name="value"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static NodeAccessor Int(int value) { return new NodeAccessor(value, 0); }
|
public static NodeAccessor Int(int value) { return new NodeAccessor(value, null, 0); }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a string node accessor
|
/// Create a string node accessor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="value"></param>
|
/// <param name="value"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static NodeAccessor String(string value) { return new NodeAccessor(value, 1); }
|
public static NodeAccessor String(string value) { return new NodeAccessor(null, value, 1); }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a property name node accessor
|
/// Create a property name node accessor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static NodeAccessor PropertyName() { return new NodeAccessor(null, 2); }
|
public static NodeAccessor PropertyName() { return new NodeAccessor(null, null, 2); }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Sockets.MessageParsing
|
namespace CryptoExchange.Net.Converters.MessageParsing
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Message access definition
|
/// Message access definition
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace CryptoExchange.Net.Sockets.MessageParsing
|
namespace CryptoExchange.Net.Converters.MessageParsing
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Message path extension methods
|
/// Message path extension methods
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace CryptoExchange.Net.Sockets.MessageParsing
|
namespace CryptoExchange.Net.Converters.MessageParsing
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Message node type
|
/// Message node type
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
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 == 12 && stringValue.StartsWith("202"))
|
||||||
|
{
|
||||||
|
// Parse 202303261200 format
|
||||||
|
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
||||||
|
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
||||||
|
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|
||||||
|
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|
||||||
|
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
|
||||||
|
{
|
||||||
|
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringValue.Length == 8)
|
||||||
|
{
|
||||||
|
// Parse 20211103 format
|
||||||
|
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
||||||
|
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
||||||
|
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
|
||||||
|
{
|
||||||
|
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringValue.Length == 6)
|
||||||
|
{
|
||||||
|
// Parse 211103 format
|
||||||
|
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|
||||||
|
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|
||||||
|
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
|
||||||
|
{
|
||||||
|
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
||||||
|
{
|
||||||
|
// Parse 1637745563.000 format
|
||||||
|
if (doubleValue < 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,21 +5,28 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<PackageId>CryptoExchange.Net</PackageId>
|
<PackageId>CryptoExchange.Net</PackageId>
|
||||||
<Authors>JKorf</Authors>
|
<Authors>JKorf</Authors>
|
||||||
<Description>A base package for implementing cryptocurrency API's</Description>
|
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||||
<PackageVersion>7.0.0-beta1</PackageVersion>
|
<PackageVersion>7.3.3</PackageVersion>
|
||||||
<AssemblyVersion>7.0.0-beta1</AssemblyVersion>
|
<AssemblyVersion>7.3.3</AssemblyVersion>
|
||||||
<FileVersion>7.0.0-beta1</FileVersion>
|
<FileVersion>7.3.3</FileVersion>
|
||||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
|
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange</PackageTags>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
||||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
||||||
<NeutralLanguage>en</NeutralLanguage>
|
<NeutralLanguage>en</NeutralLanguage>
|
||||||
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
|
<PackageIcon>icon.png</PackageIcon>
|
||||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
<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>
|
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<LangVersion>10.0</LangVersion>
|
<LangVersion>10.0</LangVersion>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</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'">
|
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
<IncludeSymbols>true</IncludeSymbols>
|
<IncludeSymbols>true</IncludeSymbols>
|
||||||
@@ -37,7 +44,7 @@
|
|||||||
<DocumentationFile>CryptoExchange.Net.xml</DocumentationFile>
|
<DocumentationFile>CryptoExchange.Net.xml</DocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0">
|
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0.1">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
@@ -45,11 +52,12 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.32" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.32" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.32" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
|
||||||
|
<PackageReference Include="System.Text.Json" Version="8.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.IO.Compression;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Security;
|
using System.Security;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Web;
|
using System.Web;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using Microsoft.Extensions.Logging;
|
using System.Globalization;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
@@ -29,18 +28,6 @@ namespace CryptoExchange.Net
|
|||||||
parameters.Add(key, value);
|
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>
|
/// <summary>
|
||||||
/// Add a parameter
|
/// Add a parameter
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -52,18 +39,6 @@ namespace CryptoExchange.Net
|
|||||||
parameters.Add(key, value);
|
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>
|
/// <summary>
|
||||||
/// Add an optional parameter. Not added if value is null
|
/// Add an optional parameter. Not added if value is null
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -76,19 +51,6 @@ namespace CryptoExchange.Net
|
|||||||
parameters.Add(key, value);
|
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>
|
/// <summary>
|
||||||
/// Create a query string of the specified parameters
|
/// Create a query string of the specified parameters
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -96,7 +58,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="urlEncodeValues">Whether or not the values should be url encoded</param>
|
/// <param name="urlEncodeValues">Whether or not the values should be url encoded</param>
|
||||||
/// <param name="serializationType">How to serialize array parameters</param>
|
/// <param name="serializationType">How to serialize array parameters</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static string CreateParamString(this Dictionary<string, object> parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType)
|
public static string CreateParamString(this IDictionary<string, object> parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType)
|
||||||
{
|
{
|
||||||
var uriString = string.Empty;
|
var uriString = string.Empty;
|
||||||
var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList();
|
var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList();
|
||||||
@@ -104,17 +66,22 @@ namespace CryptoExchange.Net
|
|||||||
{
|
{
|
||||||
if (serializationType == ArrayParametersSerialization.Array)
|
if (serializationType == ArrayParametersSerialization.Array)
|
||||||
{
|
{
|
||||||
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={v}"))}&";
|
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 += "&";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var array = (Array)arrayEntry.Value;
|
var array = (Array)arrayEntry.Value;
|
||||||
uriString += string.Join("&", array.OfType<object>().Select(a => $"{arrayEntry.Key}={Uri.EscapeDataString(a.ToString())}"));
|
uriString += $"{arrayEntry.Key}=[{string.Join(",", array.OfType<object>().Select(a => string.Format(CultureInfo.InvariantCulture, "{0}", a)))}]&";
|
||||||
uriString += "&";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uriString += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={(urlEncodeValues ? Uri.EscapeDataString(s.Value.ToString()) : s.Value)}"))}";
|
uriString += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={(urlEncodeValues ? Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", s.Value)) : string.Format(CultureInfo.InvariantCulture, "{0}", s.Value))}"))}";
|
||||||
uriString = uriString.TrimEnd('&');
|
uriString = uriString.TrimEnd('&');
|
||||||
return uriString;
|
return uriString;
|
||||||
}
|
}
|
||||||
@@ -124,7 +91,7 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="parameters"></param>
|
/// <param name="parameters"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static string ToFormData(this SortedDictionary<string, object> parameters)
|
public static string ToFormData(this IDictionary<string, object> parameters)
|
||||||
{
|
{
|
||||||
var formData = HttpUtility.ParseQueryString(string.Empty);
|
var formData = HttpUtility.ParseQueryString(string.Empty);
|
||||||
foreach (var kvp in parameters)
|
foreach (var kvp in parameters)
|
||||||
@@ -133,16 +100,15 @@ namespace CryptoExchange.Net
|
|||||||
{
|
{
|
||||||
var array = (Array)kvp.Value;
|
var array = (Array)kvp.Value;
|
||||||
foreach (var value in array)
|
foreach (var value in array)
|
||||||
formData.Add(kvp.Key, value.ToString());
|
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", value));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
formData.Add(kvp.Key, kvp.Value.ToString());
|
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return formData.ToString();
|
return formData.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the string the secure string is representing
|
/// Get the string the secure string is representing
|
||||||
@@ -230,37 +196,6 @@ namespace CryptoExchange.Net
|
|||||||
return secureString;
|
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>
|
/// <summary>
|
||||||
/// Validates an int is one of the allowed values
|
/// Validates an int is one of the allowed values
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -417,10 +352,26 @@ namespace CryptoExchange.Net
|
|||||||
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
||||||
foreach (var parameter in parameters)
|
foreach (var parameter in parameters)
|
||||||
{
|
{
|
||||||
if(parameter.Value.GetType().IsArray)
|
if (parameter.Value.GetType().IsArray)
|
||||||
{
|
{
|
||||||
foreach (var item in (object[])parameter.Value)
|
if (arraySerialization == ArrayParametersSerialization.JsonArray)
|
||||||
httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString());
|
{
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -450,8 +401,24 @@ namespace CryptoExchange.Net
|
|||||||
{
|
{
|
||||||
if (parameter.Value.GetType().IsArray)
|
if (parameter.Value.GetType().IsArray)
|
||||||
{
|
{
|
||||||
foreach (var item in (object[])parameter.Value)
|
if (arraySerialization == ArrayParametersSerialization.JsonArray)
|
||||||
httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString());
|
{
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -481,6 +448,22 @@ namespace CryptoExchange.Net
|
|||||||
|
|
||||||
return ub.Uri;
|
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.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -1,8 +1,6 @@
|
|||||||
using CryptoExchange.Net.Interfaces.CommonClients;
|
using CryptoExchange.Net.Interfaces.CommonClients;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces
|
namespace CryptoExchange.Net.Interfaces
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
using CryptoExchange.Net.Interfaces.CommonClients;
|
using System;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces
|
namespace CryptoExchange.Net.Interfaces
|
||||||
{
|
{
|
||||||
|
|||||||
+49
-6
@@ -1,8 +1,11 @@
|
|||||||
using System;
|
using CryptoExchange.Net.Converters.MessageParsing;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Sockets.MessageParsing.Interfaces
|
namespace CryptoExchange.Net.Interfaces
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Message accessor
|
/// Message accessor
|
||||||
@@ -14,14 +17,17 @@ namespace CryptoExchange.Net.Sockets.MessageParsing.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
bool IsJson { get; }
|
bool IsJson { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Is the original data available for retrieval
|
||||||
|
/// </summary>
|
||||||
|
bool OriginalDataAvailable { get; }
|
||||||
|
/// <summary>
|
||||||
/// The underlying data object
|
/// The underlying data object
|
||||||
/// </summary>
|
/// </summary>
|
||||||
object? Underlying { get; }
|
object? Underlying { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Load a stream message
|
/// Clear internal data structure
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="stream"></param>
|
void Clear();
|
||||||
void Load(Stream stream);
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the type of node
|
/// Get the type of node
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -53,6 +59,43 @@ namespace CryptoExchange.Net.Sockets.MessageParsing.Interfaces
|
|||||||
/// <param name="type"></param>
|
/// <param name="type"></param>
|
||||||
/// <param name="path"></param>
|
/// <param name="path"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
object Deserialize(Type type, MessagePath? path = null);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces
|
namespace CryptoExchange.Net.Interfaces
|
||||||
{
|
{
|
||||||
@@ -18,6 +16,10 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int Id { get; }
|
public int Id { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Whether this listener can handle data
|
||||||
|
/// </summary>
|
||||||
|
public bool CanHandleData { get; }
|
||||||
|
/// <summary>
|
||||||
/// The identifiers for this processor
|
/// The identifiers for this processor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public HashSet<string> ListenerIdentifiers { get; }
|
public HashSet<string> ListenerIdentifiers { get; }
|
||||||
@@ -27,7 +29,7 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <param name="connection"></param>
|
/// <param name="connection"></param>
|
||||||
/// <param name="message"></param>
|
/// <param name="message"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<CallResult> HandleAsync(SocketConnection connection, DataEvent<object> message);
|
CallResult Handle(SocketConnection connection, DataEvent<object> message);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the type the message should be deserialized to
|
/// Get the type the message should be deserialized to
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -35,11 +37,11 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Type? GetMessageType(IMessageAccessor messageAccessor);
|
Type? GetMessageType(IMessageAccessor messageAccessor);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deserialize a message int oobject of type
|
/// Deserialize a message into object of type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="accessor"></param>
|
/// <param name="accessor"></param>
|
||||||
/// <param name="type"></param>
|
/// <param name="type"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
object Deserialize(IMessageAccessor accessor, Type type);
|
CallResult<object> Deserialize(IMessageAccessor accessor, Type type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace CryptoExchange.Net.Sockets.MessageParsing.Interfaces
|
namespace CryptoExchange.Net.Interfaces
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Serializer interface
|
/// Serializer interface
|
||||||
@@ -17,5 +17,10 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// The total amount of requests made with this client
|
/// The total amount of requests made with this client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
int TotalRequestsMade { get; }
|
int TotalRequestsMade { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The exchange name
|
||||||
|
/// </summary>
|
||||||
|
string Exchange { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Log the current state of connections and subscriptions
|
/// Log the current state of connections and subscriptions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
string GetSubscriptionsState();
|
string GetSubscriptionsState(bool includeSubDetails = true);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reconnect all connections
|
/// Reconnect all connections
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ISocketClient: IDisposable
|
public interface ISocketClient: IDisposable
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The exchange name
|
||||||
|
/// </summary>
|
||||||
|
string Exchange { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The options provided for this client
|
/// The options provided for this client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -12,9 +12,14 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
public interface ISymbolOrderBook
|
public interface ISymbolOrderBook
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Identifier
|
/// The exchange the book is for
|
||||||
/// </summary>
|
/// </summary>
|
||||||
string Id { get; }
|
string Exchange { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The Api the book is for
|
||||||
|
/// </summary>
|
||||||
|
string Api { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The status of the order book. Order book is up to date when the status is `Synced`
|
/// The status of the order book. Order book is up to date when the status is `Synced`
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System;
|
using CryptoExchange.Net.Objects;
|
||||||
using System.IO;
|
using System;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@@ -17,12 +17,16 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Websocket message received event
|
/// Websocket message received event
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event Func<WebSocketMessageType, Stream, Task> OnStreamMessage;
|
event Action<WebSocketMessageType, ReadOnlyMemory<byte>> OnStreamMessage;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Websocket sent event, RequestId as parameter
|
/// Websocket sent event, RequestId as parameter
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event Func<int, Task> OnRequestSent;
|
event Func<int, Task> OnRequestSent;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Websocket query was ratelimited and couldn't be send
|
||||||
|
/// </summary>
|
||||||
|
event Func<int, Task>? OnRequestRateLimited;
|
||||||
|
/// <summary>
|
||||||
/// Websocket error event
|
/// Websocket error event
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event Func<Exception, Task> OnError;
|
event Func<Exception, Task> OnError;
|
||||||
@@ -67,7 +71,7 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// Connect the socket
|
/// Connect the socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<bool> ConnectAsync();
|
Task<CallResult> ConnectAsync();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send data
|
/// Send data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
+336
@@ -0,0 +1,336 @@
|
|||||||
|
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?> _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}] [Req {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");
|
||||||
|
|
||||||
|
_sentBytes = LoggerMessage.Define<int, int, int>(
|
||||||
|
LogLevel.Trace,
|
||||||
|
new EventId(1016, "SentBytes"),
|
||||||
|
"[Sckt {SocketId}] [Req {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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Logging.Extensions
|
||||||
|
{
|
||||||
|
internal static class RateLimitGateLoggingExtensions
|
||||||
|
{
|
||||||
|
private static readonly Action<ILogger, int, string, string, string, Exception?> _rateLimitRequestFailed;
|
||||||
|
private static readonly Action<ILogger, int, string, string, Exception?> _rateLimitConnectionFailed;
|
||||||
|
private static readonly Action<ILogger, int, string, TimeSpan, string, string, Exception?> _rateLimitDelayingRequest;
|
||||||
|
private static readonly Action<ILogger, int, TimeSpan, string, string, Exception?> _rateLimitDelayingConnection;
|
||||||
|
private static readonly Action<ILogger, int, string, string, string, int, Exception?> _rateLimitAppliedRequest;
|
||||||
|
private static readonly Action<ILogger, int, string, string, int, Exception?> _rateLimitAppliedConnection;
|
||||||
|
|
||||||
|
static RateLimitGateLoggingExtensions()
|
||||||
|
{
|
||||||
|
_rateLimitRequestFailed = LoggerMessage.Define<int, string, string, string>(
|
||||||
|
LogLevel.Warning,
|
||||||
|
new EventId(6000, "RateLimitRequestFailed"),
|
||||||
|
"[Req {Id}] Call to {Path} failed because of ratelimit guard {Guard}; {Limit}");
|
||||||
|
|
||||||
|
_rateLimitConnectionFailed = LoggerMessage.Define<int, string, string>(
|
||||||
|
LogLevel.Warning,
|
||||||
|
new EventId(6001, "RateLimitConnectionFailed"),
|
||||||
|
"[Sckt {Id}] Connection failed because of ratelimit guard {Guard}; {Limit}");
|
||||||
|
|
||||||
|
_rateLimitDelayingRequest = LoggerMessage.Define<int, string, TimeSpan, string, string>(
|
||||||
|
LogLevel.Warning,
|
||||||
|
new EventId(6002, "RateLimitDelayingRequest"),
|
||||||
|
"[Req {Id}] Delaying call to {Path} by {Delay} because of ratelimit guard {Guard}; {Limit}");
|
||||||
|
|
||||||
|
_rateLimitDelayingConnection = LoggerMessage.Define<int, TimeSpan, string, string>(
|
||||||
|
LogLevel.Warning,
|
||||||
|
new EventId(6003, "RateLimitDelayingConnection"),
|
||||||
|
"[Sckt {Id}] Delaying connection by {Delay} because of ratelimit guard {Guard}; {Limit}");
|
||||||
|
|
||||||
|
_rateLimitAppliedConnection = LoggerMessage.Define<int, string, string, int>(
|
||||||
|
LogLevel.Trace,
|
||||||
|
new EventId(6004, "RateLimitDelayingConnection"),
|
||||||
|
"[Sckt {Id}] Connection passed ratelimit guard {Guard}; {Limit}, New count: {Current}");
|
||||||
|
|
||||||
|
_rateLimitAppliedRequest = LoggerMessage.Define<int, string, string, string, int>(
|
||||||
|
LogLevel.Trace,
|
||||||
|
new EventId(6005, "RateLimitAppliedRequest"),
|
||||||
|
"[Req {Id}] Call to {Path} passed ratelimit guard {Guard}; {Limit}, New count: {Current}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RateLimitRequestFailed(this ILogger logger, int requestId, string path, string guard, string limit)
|
||||||
|
{
|
||||||
|
_rateLimitRequestFailed(logger, requestId, path, guard, limit, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RateLimitConnectionFailed(this ILogger logger, int connectionId, string guard, string limit)
|
||||||
|
{
|
||||||
|
_rateLimitConnectionFailed(logger, connectionId, guard, limit, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RateLimitDelayingRequest(this ILogger logger, int requestId, string path, TimeSpan delay, string guard, string limit)
|
||||||
|
{
|
||||||
|
_rateLimitDelayingRequest(logger, requestId, path, delay, guard, limit, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RateLimitDelayingConnection(this ILogger logger, int connectionId, TimeSpan delay, string guard, string limit)
|
||||||
|
{
|
||||||
|
_rateLimitDelayingConnection(logger, connectionId, delay, guard, limit, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RateLimitAppliedConnection(this ILogger logger, int connectionId, string guard, string limit, int current)
|
||||||
|
{
|
||||||
|
_rateLimitAppliedConnection(logger, connectionId, guard, limit, current, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RateLimitAppliedRequest(this ILogger logger, int requestIdId, string path, string guard, string limit, int current)
|
||||||
|
{
|
||||||
|
_rateLimitAppliedRequest(logger, requestIdId, path, guard, limit, current, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
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;
|
||||||
|
private static readonly Action<ILogger, int, DateTime, Exception?> _restApiRateLimitRetry;
|
||||||
|
private static readonly Action<ILogger, int, DateTime, Exception?> _restApiRateLimitPauseUntil;
|
||||||
|
private static readonly Action<ILogger, int, RequestDefinition, string?, string, string, Exception?> _restApiSendRequest;
|
||||||
|
|
||||||
|
|
||||||
|
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}");
|
||||||
|
|
||||||
|
_restApiRateLimitRetry = LoggerMessage.Define<int, DateTime>(
|
||||||
|
LogLevel.Warning,
|
||||||
|
new EventId(4006, "RestApiRateLimitRetry"),
|
||||||
|
"[Req {RequestId}] Received ratelimit error, retrying after {Timestamp}");
|
||||||
|
|
||||||
|
_restApiRateLimitPauseUntil = LoggerMessage.Define<int, DateTime>(
|
||||||
|
LogLevel.Warning,
|
||||||
|
new EventId(4007, "RestApiRateLimitPauseUntil"),
|
||||||
|
"[Req {RequestId}] Ratelimit error from server, pausing requests until {Until}");
|
||||||
|
|
||||||
|
_restApiSendRequest = LoggerMessage.Define<int, RequestDefinition, string?, string, string>(
|
||||||
|
LogLevel.Debug,
|
||||||
|
new EventId(4008, "RestApiSendRequest"),
|
||||||
|
"[Req {RequestId}] Sending {Definition} request with body {Body}, query parameters {Query} and headers {Headers}");
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RestApiRateLimitRetry(this ILogger logger, int requestId, DateTime retryAfter)
|
||||||
|
{
|
||||||
|
_restApiRateLimitRetry(logger, requestId, retryAfter, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RestApiRateLimitPauseUntil(this ILogger logger, int requestId, DateTime retryAfter)
|
||||||
|
{
|
||||||
|
_restApiRateLimitPauseUntil(logger, requestId, retryAfter, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RestApiSendRequest(this ILogger logger, int requestId, RequestDefinition definition, string? body, string query, string headers)
|
||||||
|
{
|
||||||
|
_restApiSendRequest(logger, requestId, definition, body, query, headers, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
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}] [Req {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}] [Req {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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,29 @@
|
|||||||
Wait
|
Wait
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// What to do when a request would exceed the rate limit
|
||||||
|
/// </summary>
|
||||||
|
public enum RateLimitWindowType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A sliding window
|
||||||
|
/// </summary>
|
||||||
|
Sliding,
|
||||||
|
/// <summary>
|
||||||
|
/// A fixed interval window
|
||||||
|
/// </summary>
|
||||||
|
Fixed,
|
||||||
|
/// <summary>
|
||||||
|
/// A fixed interval starting after the first request
|
||||||
|
/// </summary>
|
||||||
|
FixedAfterFirst,
|
||||||
|
/// <summary>
|
||||||
|
/// Decaying window
|
||||||
|
/// </summary>
|
||||||
|
Decay
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Where the parameters for a HttpMethod should be added in a request
|
/// Where the parameters for a HttpMethod should be added in a request
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -99,15 +122,22 @@
|
|||||||
/// Define how array parameters should be send
|
/// Define how array parameters should be send
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum ArrayParametersSerialization
|
public enum ArrayParametersSerialization
|
||||||
|
#pragma warning disable CS1570 // XML comment has badly formed XML
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send multiple key=value for each entry
|
/// Send as key=value1&key=value2
|
||||||
/// </summary>
|
/// </summary>
|
||||||
MultipleValues,
|
MultipleValues,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create an []=value array
|
/// Send as key[]=value1&key[]=value2
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Array
|
Array,
|
||||||
|
/// <summary>
|
||||||
|
/// Send as key=[value1, value2]
|
||||||
|
/// </summary>
|
||||||
|
JsonArray
|
||||||
|
#pragma warning restore CS1570 // XML comment has badly formed XML
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -28,6 +28,15 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ApiCredentials? ApiCredentials { get; set; }
|
public ApiCredentials? ApiCredentials { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether or not client side rate limiting should be applied
|
||||||
|
/// </summary>
|
||||||
|
public bool RateLimiterEnabled { get; set; } = true;
|
||||||
|
/// <summary>
|
||||||
|
/// What should happen when a rate limit is reached
|
||||||
|
/// </summary>
|
||||||
|
public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Objects.Options
|
namespace CryptoExchange.Net.Objects.Options
|
||||||
{
|
{
|
||||||
@@ -10,16 +8,6 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class RestApiOptions : ApiOptions
|
public class RestApiOptions : ApiOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// List of rate limiters to use
|
|
||||||
/// </summary>
|
|
||||||
public List<IRateLimiter> RateLimiters { get; set; } = new List<IRateLimiter>();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// What to do when a call would exceed the rate limit
|
|
||||||
/// </summary>
|
|
||||||
public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether or not to automatically sync the local time with the server time
|
/// Whether or not to automatically sync the local time with the server time
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -42,8 +30,6 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
ApiCredentials = ApiCredentials?.Copy(),
|
ApiCredentials = ApiCredentials?.Copy(),
|
||||||
OutputOriginalData = OutputOriginalData,
|
OutputOriginalData = OutputOriginalData,
|
||||||
AutoTimestamp = AutoTimestamp,
|
AutoTimestamp = AutoTimestamp,
|
||||||
RateLimiters = RateLimiters,
|
|
||||||
RateLimitingBehaviour = RateLimitingBehaviour,
|
|
||||||
TimestampRecalculationInterval = TimestampRecalculationInterval
|
TimestampRecalculationInterval = TimestampRecalculationInterval
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
TimestampRecalculationInterval = TimestampRecalculationInterval,
|
TimestampRecalculationInterval = TimestampRecalculationInterval,
|
||||||
ApiCredentials = ApiCredentials?.Copy(),
|
ApiCredentials = ApiCredentials?.Copy(),
|
||||||
Proxy = Proxy,
|
Proxy = Proxy,
|
||||||
RequestTimeout = RequestTimeout
|
RequestTimeout = RequestTimeout,
|
||||||
|
RateLimiterEnabled = RateLimiterEnabled,
|
||||||
|
RateLimitingBehaviour = RateLimitingBehaviour
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Objects.Options
|
namespace CryptoExchange.Net.Objects.Options
|
||||||
{
|
{
|
||||||
@@ -10,11 +8,6 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class SocketApiOptions : ApiOptions
|
public class SocketApiOptions : ApiOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// List of rate limiters to use
|
|
||||||
/// </summary>
|
|
||||||
public List<IRateLimiter> RateLimiters { get; set; } = new List<IRateLimiter>();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The max time of not receiving any data after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected,
|
/// The max time of not receiving any data after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected,
|
||||||
/// for example when the server sends intermittent ping requests
|
/// for example when the server sends intermittent ping requests
|
||||||
@@ -37,7 +30,6 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
{
|
{
|
||||||
ApiCredentials = ApiCredentials?.Copy(),
|
ApiCredentials = ApiCredentials?.Copy(),
|
||||||
OutputOriginalData = OutputOriginalData,
|
OutputOriginalData = OutputOriginalData,
|
||||||
RateLimiters = RateLimiters,
|
|
||||||
SocketNoDataTimeout = SocketNoDataTimeout,
|
SocketNoDataTimeout = SocketNoDataTimeout,
|
||||||
MaxSocketConnections = MaxSocketConnections,
|
MaxSocketConnections = MaxSocketConnections,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -65,7 +65,9 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget,
|
SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget,
|
||||||
MaxSocketConnections = MaxSocketConnections,
|
MaxSocketConnections = MaxSocketConnections,
|
||||||
Proxy = Proxy,
|
Proxy = Proxy,
|
||||||
RequestTimeout = RequestTimeout
|
RequestTimeout = RequestTimeout,
|
||||||
|
RateLimitingBehaviour = RateLimitingBehaviour,
|
||||||
|
RateLimiterEnabled = RateLimiterEnabled,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using CryptoExchange.Net.Attributes;
|
using CryptoExchange.Net.Attributes;
|
||||||
using CryptoExchange.Net.Converters;
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
@@ -158,6 +158,17 @@ namespace CryptoExchange.Net.Objects
|
|||||||
Add(key, EnumConverter.GetString(value)!);
|
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>
|
/// <summary>
|
||||||
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />. Not added if value is null
|
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />. Not added if value is null
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -168,5 +179,19 @@ namespace CryptoExchange.Net.Objects
|
|||||||
if (value != null)
|
if (value != null)
|
||||||
Add(key, EnumConverter.GetString(value));
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,443 +0,0 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Security;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Objects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Limits the amount of requests to a certain constraint
|
|
||||||
/// </summary>
|
|
||||||
public class RateLimiter : IRateLimiter
|
|
||||||
{
|
|
||||||
private readonly object _limiterLock = new object();
|
|
||||||
internal List<Limiter> _limiters = new List<Limiter>();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a new RateLimiter. Configure the rate limiter by calling <see cref="AddTotalRateLimit"/>,
|
|
||||||
/// <see cref="AddEndpointLimit(string, int, TimeSpan, HttpMethod?, bool)"/>, <see cref="AddPartialEndpointLimit(string, int, TimeSpan, HttpMethod?, bool, bool)"/> or <see cref="AddApiKeyLimit"/>.
|
|
||||||
/// </summary>
|
|
||||||
public RateLimiter()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add a rate limit for the total amount of requests per time period
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
|
|
||||||
/// <param name="perTimePeriod">The time period the limit is for</param>
|
|
||||||
public RateLimiter AddTotalRateLimit(int limit, TimeSpan perTimePeriod)
|
|
||||||
{
|
|
||||||
lock(_limiterLock)
|
|
||||||
_limiters.Add(new TotalRateLimiter(limit, perTimePeriod, null));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add a rate lmit for the amount of requests per time for an endpoint
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="endpoint">The endpoint the limit is for</param>
|
|
||||||
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
|
|
||||||
/// <param name="perTimePeriod">The time period the limit is for</param>
|
|
||||||
/// <param name="method">The HttpMethod the limit is for, null for all</param>
|
|
||||||
/// <param name="excludeFromOtherRateLimits">If set to true it ignores other rate limits</param>
|
|
||||||
public RateLimiter AddEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
|
|
||||||
{
|
|
||||||
lock(_limiterLock)
|
|
||||||
_limiters.Add(new EndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, excludeFromOtherRateLimits));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add a rate lmit for the amount of requests per time for an endpoint
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="endpoints">The endpoints the limit is for</param>
|
|
||||||
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
|
|
||||||
/// <param name="perTimePeriod">The time period the limit is for</param>
|
|
||||||
/// <param name="method">The HttpMethod the limit is for, null for all</param>
|
|
||||||
/// <param name="excludeFromOtherRateLimits">If set to true it ignores other rate limits</param>
|
|
||||||
public RateLimiter AddEndpointLimit(IEnumerable<string> endpoints, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
|
|
||||||
{
|
|
||||||
lock(_limiterLock)
|
|
||||||
_limiters.Add(new EndpointRateLimiter(endpoints.ToArray(), limit, perTimePeriod, method, excludeFromOtherRateLimits));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add a rate lmit for the amount of requests per time for an endpoint
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="endpoint">The endpoint the limit is for</param>
|
|
||||||
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
|
|
||||||
/// <param name="perTimePeriod">The time period the limit is for</param>
|
|
||||||
/// <param name="method">The HttpMethod the limit is for, null for all</param>
|
|
||||||
/// <param name="ignoreOtherRateLimits">If set to true it ignores other rate limits</param>
|
|
||||||
/// <param name="countPerEndpoint">Whether all requests for this partial endpoint are bound to the same limit or each individual endpoint has its own limit</param>
|
|
||||||
public RateLimiter AddPartialEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool countPerEndpoint = false, bool ignoreOtherRateLimits = false)
|
|
||||||
{
|
|
||||||
lock(_limiterLock)
|
|
||||||
_limiters.Add(new PartialEndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, ignoreOtherRateLimits, countPerEndpoint));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add a rate limit for the amount of requests per Api key
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
|
|
||||||
/// <param name="perTimePeriod">The time period the limit is for</param>
|
|
||||||
/// <param name="onlyForSignedRequests">Only include calls that are signed in this limiter</param>
|
|
||||||
/// <param name="excludeFromTotalRateLimit">Exclude requests with API key from the total rate limiter</param>
|
|
||||||
public RateLimiter AddApiKeyLimit(int limit, TimeSpan perTimePeriod, bool onlyForSignedRequests, bool excludeFromTotalRateLimit)
|
|
||||||
{
|
|
||||||
lock(_limiterLock)
|
|
||||||
_limiters.Add(new ApiKeyRateLimiter(limit, perTimePeriod, null, onlyForSignedRequests, excludeFromTotalRateLimit));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add a rate limit for the amount of messages that can be send per connection
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="endpoint">The endpoint that the limit is for</param>
|
|
||||||
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
|
|
||||||
/// <param name="perTimePeriod">The time period the limit is for</param>
|
|
||||||
public RateLimiter AddConnectionRateLimit(string endpoint, int limit, TimeSpan perTimePeriod)
|
|
||||||
{
|
|
||||||
lock (_limiterLock)
|
|
||||||
_limiters.Add(new ConnectionRateLimiter(new[] { endpoint }, limit, perTimePeriod));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<CallResult<int>> LimitRequestAsync(ILogger logger, string endpoint, HttpMethod method, bool signed, SecureString? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct)
|
|
||||||
{
|
|
||||||
int totalWaitTime = 0;
|
|
||||||
|
|
||||||
List<EndpointRateLimiter> endpointLimits;
|
|
||||||
lock (_limiterLock)
|
|
||||||
endpointLimits = _limiters.OfType<EndpointRateLimiter>().Where(h => h.Endpoints.Contains(endpoint) && (h.Method == null || h.Method == method)).ToList();
|
|
||||||
foreach (var endpointLimit in endpointLimits)
|
|
||||||
{
|
|
||||||
var waitResult = await ProcessTopic(logger, endpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
|
||||||
if (!waitResult)
|
|
||||||
return waitResult;
|
|
||||||
|
|
||||||
totalWaitTime += waitResult.Data;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (endpointLimits.Any(l => l.IgnoreOtherRateLimits))
|
|
||||||
return new CallResult<int>(totalWaitTime);
|
|
||||||
|
|
||||||
List<PartialEndpointRateLimiter> partialEndpointLimits;
|
|
||||||
lock (_limiterLock)
|
|
||||||
partialEndpointLimits = _limiters.OfType<PartialEndpointRateLimiter>().Where(h => h.PartialEndpoints.Any(h => endpoint.Contains(h)) && (h.Method == null || h.Method == method)).ToList();
|
|
||||||
foreach (var partialEndpointLimit in partialEndpointLimits)
|
|
||||||
{
|
|
||||||
if (partialEndpointLimit.CountPerEndpoint)
|
|
||||||
{
|
|
||||||
SingleTopicRateLimiter? thisEndpointLimit;
|
|
||||||
lock (_limiterLock)
|
|
||||||
{
|
|
||||||
thisEndpointLimit = _limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.PartialEndpoint && (string)h.Topic == endpoint);
|
|
||||||
if (thisEndpointLimit == null)
|
|
||||||
{
|
|
||||||
thisEndpointLimit = new SingleTopicRateLimiter(endpoint, partialEndpointLimit);
|
|
||||||
_limiters.Add(thisEndpointLimit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var waitResult = await ProcessTopic(logger, thisEndpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
|
||||||
if (!waitResult)
|
|
||||||
return waitResult;
|
|
||||||
|
|
||||||
totalWaitTime += waitResult.Data;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var waitResult = await ProcessTopic(logger, partialEndpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
|
||||||
if (!waitResult)
|
|
||||||
return waitResult;
|
|
||||||
|
|
||||||
totalWaitTime += waitResult.Data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(partialEndpointLimits.Any(p => p.IgnoreOtherRateLimits))
|
|
||||||
return new CallResult<int>(totalWaitTime);
|
|
||||||
|
|
||||||
List<ApiKeyRateLimiter> apiLimits;
|
|
||||||
lock (_limiterLock)
|
|
||||||
apiLimits = _limiters.OfType<ApiKeyRateLimiter>().Where(h => h.Type == RateLimitType.ApiKey).ToList();
|
|
||||||
foreach (var apiLimit in apiLimits)
|
|
||||||
{
|
|
||||||
if(apiKey == null)
|
|
||||||
{
|
|
||||||
if (!apiLimit.OnlyForSignedRequests)
|
|
||||||
{
|
|
||||||
var waitResult = await ProcessTopic(logger, apiLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
|
||||||
if (!waitResult)
|
|
||||||
return waitResult;
|
|
||||||
|
|
||||||
totalWaitTime += waitResult.Data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (signed || !apiLimit.OnlyForSignedRequests)
|
|
||||||
{
|
|
||||||
SingleTopicRateLimiter? thisApiLimit;
|
|
||||||
lock (_limiterLock)
|
|
||||||
{
|
|
||||||
thisApiLimit = _limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey && ((SecureString)h.Topic).IsEqualTo(apiKey));
|
|
||||||
if (thisApiLimit == null)
|
|
||||||
{
|
|
||||||
thisApiLimit = new SingleTopicRateLimiter(apiKey, apiLimit);
|
|
||||||
_limiters.Add(thisApiLimit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var waitResult = await ProcessTopic(logger, thisApiLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
|
||||||
if (!waitResult)
|
|
||||||
return waitResult;
|
|
||||||
|
|
||||||
totalWaitTime += waitResult.Data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((signed || apiLimits.All(l => !l.OnlyForSignedRequests)) && apiLimits.Any(l => l.IgnoreTotalRateLimit))
|
|
||||||
return new CallResult<int>(totalWaitTime);
|
|
||||||
|
|
||||||
List<TotalRateLimiter> totalLimits;
|
|
||||||
lock (_limiterLock)
|
|
||||||
totalLimits = _limiters.OfType<TotalRateLimiter>().ToList();
|
|
||||||
foreach(var totalLimit in totalLimits)
|
|
||||||
{
|
|
||||||
var waitResult = await ProcessTopic(logger, totalLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
|
||||||
if (!waitResult)
|
|
||||||
return waitResult;
|
|
||||||
|
|
||||||
totalWaitTime += waitResult.Data;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new CallResult<int>(totalWaitTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<CallResult<int>> ProcessTopic(ILogger logger, Limiter historyTopic, string endpoint, int requestWeight, RateLimitingBehaviour limitBehaviour, CancellationToken ct)
|
|
||||||
{
|
|
||||||
var sw = Stopwatch.StartNew();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await historyTopic.Semaphore.WaitAsync(ct).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
return new CallResult<int>(new CancellationRequestedError());
|
|
||||||
}
|
|
||||||
sw.Stop();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
int totalWaitTime = 0;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
// Remove requests no longer in time period from the history
|
|
||||||
var checkTime = DateTime.UtcNow;
|
|
||||||
for (var i = 0; i < historyTopic.Entries.Count; i++)
|
|
||||||
{
|
|
||||||
if (historyTopic.Entries[i].Timestamp < checkTime - historyTopic.Period)
|
|
||||||
{
|
|
||||||
historyTopic.Entries.Remove(historyTopic.Entries[i]);
|
|
||||||
i--;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentWeight = !historyTopic.Entries.Any() ? 0 : historyTopic.Entries.Sum(h => h.Weight);
|
|
||||||
if (currentWeight + requestWeight > historyTopic.Limit)
|
|
||||||
{
|
|
||||||
if (currentWeight == 0)
|
|
||||||
throw new Exception("Request limit reached without any prior request. " +
|
|
||||||
$"This request can never execute with the current rate limiter. Request weight: {requestWeight}, Ratelimit: {historyTopic.Limit}");
|
|
||||||
|
|
||||||
// Wait until the next entry should be removed from the history
|
|
||||||
var thisWaitTime = (int)Math.Round(((historyTopic.Entries.First().Timestamp + historyTopic.Period) - checkTime).TotalMilliseconds);
|
|
||||||
if (thisWaitTime > 0)
|
|
||||||
{
|
|
||||||
if (limitBehaviour == RateLimitingBehaviour.Fail)
|
|
||||||
{
|
|
||||||
var msg = $"Request to {endpoint} failed because of rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}";
|
|
||||||
logger.Log(LogLevel.Warning, msg);
|
|
||||||
return new CallResult<int>(new ClientRateLimitError(msg) { RetryAfter = DateTime.UtcNow.AddSeconds(thisWaitTime) });
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Log(LogLevel.Information, $"Message to {endpoint} waiting {thisWaitTime}ms for rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await Task.Delay(thisWaitTime, ct).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
return new CallResult<int>(new CancellationRequestedError());
|
|
||||||
}
|
|
||||||
totalWaitTime += thisWaitTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var newTime = DateTime.UtcNow;
|
|
||||||
historyTopic.Entries.Add(new LimitEntry(newTime, requestWeight));
|
|
||||||
return new CallResult<int>(totalWaitTime);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
historyTopic.Semaphore.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal struct LimitEntry
|
|
||||||
{
|
|
||||||
public DateTime Timestamp { get; set; }
|
|
||||||
public int Weight { get; set; }
|
|
||||||
|
|
||||||
public LimitEntry(DateTime timestamp, int weight)
|
|
||||||
{
|
|
||||||
Timestamp = timestamp;
|
|
||||||
Weight = weight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class Limiter
|
|
||||||
{
|
|
||||||
public RateLimitType Type { get; set; }
|
|
||||||
public HttpMethod? Method { get; set; }
|
|
||||||
|
|
||||||
public SemaphoreSlim Semaphore { get; set; }
|
|
||||||
public int Limit { get; set; }
|
|
||||||
|
|
||||||
public TimeSpan Period { get; set; }
|
|
||||||
public List<LimitEntry> Entries { get; set; } = new List<LimitEntry>();
|
|
||||||
|
|
||||||
public Limiter(RateLimitType type, int limit, TimeSpan perPeriod, HttpMethod? method)
|
|
||||||
{
|
|
||||||
Semaphore = new SemaphoreSlim(1, 1);
|
|
||||||
Type = type;
|
|
||||||
Limit = limit;
|
|
||||||
Period = perPeriod;
|
|
||||||
Method = method;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class TotalRateLimiter : Limiter
|
|
||||||
{
|
|
||||||
public TotalRateLimiter(int limit, TimeSpan perPeriod, HttpMethod? method)
|
|
||||||
: base(RateLimitType.Total, limit, perPeriod, method)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return nameof(TotalRateLimiter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class ConnectionRateLimiter : PartialEndpointRateLimiter
|
|
||||||
{
|
|
||||||
public ConnectionRateLimiter(int limit, TimeSpan perPeriod)
|
|
||||||
: base(new[] { "/" }, limit, perPeriod, null, true, true)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public ConnectionRateLimiter(string[] endpoints, int limit, TimeSpan perPeriod)
|
|
||||||
: base(endpoints, limit, perPeriod, null, true, true)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return nameof(ConnectionRateLimiter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class EndpointRateLimiter: Limiter
|
|
||||||
{
|
|
||||||
public string[] Endpoints { get; set; }
|
|
||||||
public bool IgnoreOtherRateLimits { get; set; }
|
|
||||||
|
|
||||||
public EndpointRateLimiter(string[] endpoints, int limit, TimeSpan perPeriod, HttpMethod? method, bool ignoreOtherRateLimits)
|
|
||||||
:base(RateLimitType.Endpoint, limit, perPeriod, method)
|
|
||||||
{
|
|
||||||
Endpoints = endpoints;
|
|
||||||
IgnoreOtherRateLimits = ignoreOtherRateLimits;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return nameof(EndpointRateLimiter) + $": {string.Join(", ", Endpoints)}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class PartialEndpointRateLimiter : Limiter
|
|
||||||
{
|
|
||||||
public string[] PartialEndpoints { get; set; }
|
|
||||||
public bool IgnoreOtherRateLimits { get; set; }
|
|
||||||
public bool CountPerEndpoint { get; set; }
|
|
||||||
|
|
||||||
public PartialEndpointRateLimiter(string[] partialEndpoints, int limit, TimeSpan perPeriod, HttpMethod? method, bool ignoreOtherRateLimits, bool countPerEndpoint)
|
|
||||||
: base(RateLimitType.PartialEndpoint, limit, perPeriod, method)
|
|
||||||
{
|
|
||||||
PartialEndpoints = partialEndpoints;
|
|
||||||
IgnoreOtherRateLimits = ignoreOtherRateLimits;
|
|
||||||
CountPerEndpoint = countPerEndpoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return nameof(PartialEndpointRateLimiter) + $": {string.Join(", ", PartialEndpoints)}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class ApiKeyRateLimiter : Limiter
|
|
||||||
{
|
|
||||||
public bool OnlyForSignedRequests { get; set; }
|
|
||||||
public bool IgnoreTotalRateLimit { get; set; }
|
|
||||||
|
|
||||||
public ApiKeyRateLimiter(int limit, TimeSpan perPeriod, HttpMethod? method, bool onlyForSignedRequests, bool ignoreTotalRateLimit)
|
|
||||||
:base(RateLimitType.ApiKey, limit, perPeriod, method)
|
|
||||||
{
|
|
||||||
OnlyForSignedRequests = onlyForSignedRequests;
|
|
||||||
IgnoreTotalRateLimit = ignoreTotalRateLimit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class SingleTopicRateLimiter: Limiter
|
|
||||||
{
|
|
||||||
public object Topic { get; set; }
|
|
||||||
|
|
||||||
public SingleTopicRateLimiter(object topic, Limiter limiter)
|
|
||||||
:base(limiter.Type, limiter.Limit, limiter.Period, limiter.Method)
|
|
||||||
{
|
|
||||||
Topic = topic;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return (Type == RateLimitType.ApiKey ? nameof(ApiKeyRateLimiter): nameof(EndpointRateLimiter)) + $": {Topic}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal enum RateLimitType
|
|
||||||
{
|
|
||||||
Total,
|
|
||||||
Endpoint,
|
|
||||||
PartialEndpoint,
|
|
||||||
ApiKey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Objects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The definition of a rest request
|
||||||
|
/// </summary>
|
||||||
|
public class RequestDefinition
|
||||||
|
{
|
||||||
|
private string? _stringRep;
|
||||||
|
|
||||||
|
// Basics
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Path of the request
|
||||||
|
/// </summary>
|
||||||
|
public string Path { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Http method of the request
|
||||||
|
/// </summary>
|
||||||
|
public HttpMethod Method { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Is the request authenticated
|
||||||
|
/// </summary>
|
||||||
|
public bool Authenticated { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
// Formating
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The body format for this request
|
||||||
|
/// </summary>
|
||||||
|
public RequestBodyFormat? RequestBodyFormat { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The position of parameters for this request
|
||||||
|
/// </summary>
|
||||||
|
public HttpMethodParameterPosition? ParameterPosition { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The array serialization type for this request
|
||||||
|
/// </summary>
|
||||||
|
public ArrayParametersSerialization? ArraySerialization { get; set; }
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request weight
|
||||||
|
/// </summary>
|
||||||
|
public int Weight { get; set; } = 1;
|
||||||
|
/// <summary>
|
||||||
|
/// Rate limit gate to use
|
||||||
|
/// </summary>
|
||||||
|
public IRateLimitGate? RateLimitGate { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Rate limit for this specific endpoint
|
||||||
|
/// </summary>
|
||||||
|
public int? EndpointLimitCount { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Rate limit period for this specific endpoint
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? EndpointLimitPeriod { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path"></param>
|
||||||
|
/// <param name="method"></param>
|
||||||
|
public RequestDefinition(string path, HttpMethod method)
|
||||||
|
{
|
||||||
|
Path = path;
|
||||||
|
Method = method;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return _stringRep ??= $"{Method} {Path}{(Authenticated ? " authenticated" : "")}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Objects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Request definitions cache
|
||||||
|
/// </summary>
|
||||||
|
public class RequestDefinitionCache
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<string, RequestDefinition> _definitions = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="method">The HttpMethod</param>
|
||||||
|
/// <param name="path">Endpoint path</param>
|
||||||
|
/// <param name="authenticated">Endpoint is authenticated</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public RequestDefinition GetOrCreate(HttpMethod method, string path, bool authenticated = false)
|
||||||
|
=> GetOrCreate(method, path, null, 0, authenticated, null, null, null, null, null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="method">The HttpMethod</param>
|
||||||
|
/// <param name="path">Endpoint path</param>
|
||||||
|
/// <param name="rateLimitGate">The rate limit gate</param>
|
||||||
|
/// <param name="weight">Request weight</param>
|
||||||
|
/// <param name="authenticated">Endpoint is authenticated</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public RequestDefinition GetOrCreate(HttpMethod method, string path, IRateLimitGate rateLimitGate, int weight = 1, bool authenticated = false)
|
||||||
|
=> GetOrCreate(method, path, rateLimitGate, weight, authenticated, null, null, null, null, null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="method">The HttpMethod</param>
|
||||||
|
/// <param name="path">Endpoint path</param>
|
||||||
|
/// <param name="rateLimitGate">The rate limit gate</param>
|
||||||
|
/// <param name="endpointLimitCount">The limit count for this specific endpoint</param>
|
||||||
|
/// <param name="endpointLimitPeriod">The period for the limit for this specific endpoint</param>
|
||||||
|
/// <param name="weight">Request weight</param>
|
||||||
|
/// <param name="authenticated">Endpoint is authenticated</param>
|
||||||
|
/// <param name="requestBodyFormat">Request body format</param>
|
||||||
|
/// <param name="parameterPosition">Parameter position</param>
|
||||||
|
/// <param name="arraySerialization">Array serialization type</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public RequestDefinition GetOrCreate(
|
||||||
|
HttpMethod method,
|
||||||
|
string path,
|
||||||
|
IRateLimitGate? rateLimitGate,
|
||||||
|
int weight,
|
||||||
|
bool authenticated,
|
||||||
|
int? endpointLimitCount = null,
|
||||||
|
TimeSpan? endpointLimitPeriod = null,
|
||||||
|
RequestBodyFormat? requestBodyFormat = null,
|
||||||
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
|
ArrayParametersSerialization? arraySerialization = null)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (!_definitions.TryGetValue(method + path, out var def))
|
||||||
|
{
|
||||||
|
def = new RequestDefinition(path, method)
|
||||||
|
{
|
||||||
|
Authenticated = authenticated,
|
||||||
|
EndpointLimitCount = endpointLimitCount,
|
||||||
|
EndpointLimitPeriod = endpointLimitPeriod,
|
||||||
|
RateLimitGate = rateLimitGate,
|
||||||
|
Weight = weight,
|
||||||
|
ArraySerialization = arraySerialization,
|
||||||
|
RequestBodyFormat = requestBodyFormat,
|
||||||
|
ParameterPosition = parameterPosition,
|
||||||
|
};
|
||||||
|
_definitions.TryAdd(method + path, def);
|
||||||
|
}
|
||||||
|
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -51,9 +51,13 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
public TimeSpan? KeepAliveInterval { get; set; }
|
public TimeSpan? KeepAliveInterval { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The rate limiters for the socket connection
|
/// The rate limiter for the socket connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<IRateLimiter>? RateLimiters { get; set; }
|
public IRateLimitGate? RateLimiter { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// What to do when rate limit is reached
|
||||||
|
/// </summary>
|
||||||
|
public RateLimitingBehaviour RateLimitingBehaviour { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Encoding for sending/receiving data
|
/// Encoding for sending/receiving data
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class TraceLogger : ILogger
|
public class TraceLogger : ILogger
|
||||||
{
|
{
|
||||||
private string? _categoryName;
|
private readonly string? _categoryName;
|
||||||
private LogLevel _logLevel;
|
private readonly LogLevel _logLevel;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@@ -46,14 +46,14 @@ namespace CryptoExchange.Net.Objects
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IDisposable BeginScope<TState>(TState state) => null!;
|
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null!;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool IsEnabled(LogLevel logLevel) => (int)logLevel < (int)_logLevel;
|
public bool IsEnabled(LogLevel logLevel) => (int)logLevel >= (int)_logLevel;
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||||
{
|
{
|
||||||
if ((int)logLevel < (int)_logLevel)
|
if (!IsEnabled(logLevel))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {(_categoryName == null ? "" : $"{_categoryName} | ")}{formatter(state, exception)}";
|
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {(_categoryName == null ? "" : $"{_categoryName} | ")}{formatter(state, exception)}";
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ using System.Text;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Logging.Extensions;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.OrderBook
|
namespace CryptoExchange.Net.OrderBook
|
||||||
{
|
{
|
||||||
@@ -88,7 +90,10 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
protected int? Levels { get; set; } = null;
|
protected int? Levels { get; set; } = null;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string Id { get; }
|
public string Exchange { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string Api { get; }
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public OrderBookStatus Status
|
public OrderBookStatus Status
|
||||||
@@ -101,7 +106,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
|
|
||||||
var old = _status;
|
var old = _status;
|
||||||
_status = value;
|
_status = value;
|
||||||
_logger.Log(LogLevel.Information, $"{Id} order book {Symbol} status changed: {old} => {value}");
|
_logger.OrderBookStatusChanged(Api, Symbol, old, value);
|
||||||
OnStatusChange?.Invoke(old, _status);
|
OnStatusChange?.Invoke(old, _status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,14 +197,17 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">Logger to use. If not provided will create a TraceLogger</param>
|
/// <param name="logger">Logger to use. If not provided will create a TraceLogger</param>
|
||||||
/// <param name="id">The id of the order book. Should be set to {Exchange}[{type}], for example: Kucoin[Spot]</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="symbol">The symbol the order book is for</param>
|
/// <param name="symbol">The symbol the order book is for</param>
|
||||||
protected SymbolOrderBook(ILogger? logger, string id, string symbol)
|
protected SymbolOrderBook(ILoggerFactory? logger, string exchange, string api, string symbol)
|
||||||
{
|
{
|
||||||
if (symbol == null)
|
if (symbol == null)
|
||||||
throw new ArgumentNullException(nameof(symbol));
|
throw new ArgumentNullException(nameof(symbol));
|
||||||
|
|
||||||
Id = id;
|
Exchange = exchange;
|
||||||
|
Api = api;
|
||||||
|
|
||||||
_processBuffer = new List<ProcessBufferRangeSequenceEntry>();
|
_processBuffer = new List<ProcessBufferRangeSequenceEntry>();
|
||||||
_processQueue = new ConcurrentQueue<object>();
|
_processQueue = new ConcurrentQueue<object>();
|
||||||
_queueEvent = new AsyncResetEvent(false, true);
|
_queueEvent = new AsyncResetEvent(false, true);
|
||||||
@@ -210,7 +218,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
_asks = new SortedList<decimal, ISymbolOrderBookEntry>();
|
_asks = new SortedList<decimal, ISymbolOrderBookEntry>();
|
||||||
_bids = new SortedList<decimal, ISymbolOrderBookEntry>(new DescComparer<decimal>());
|
_bids = new SortedList<decimal, ISymbolOrderBookEntry>(new DescComparer<decimal>());
|
||||||
|
|
||||||
_logger = logger ?? new TraceLogger();
|
_logger = logger?.CreateLogger(Exchange) ?? NullLoggerFactory.Instance.CreateLogger(Exchange);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -232,7 +240,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
if (Status != OrderBookStatus.Disconnected)
|
if (Status != OrderBookStatus.Disconnected)
|
||||||
throw new InvalidOperationException($"Can't start book unless state is {OrderBookStatus.Disconnected}. Current state: {Status}");
|
throw new InvalidOperationException($"Can't start book unless state is {OrderBookStatus.Disconnected}. Current state: {Status}");
|
||||||
|
|
||||||
_logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} starting");
|
_logger.OrderBookStarting(Api, Symbol);
|
||||||
_cts = new CancellationTokenSource();
|
_cts = new CancellationTokenSource();
|
||||||
ct?.Register(async () =>
|
ct?.Register(async () =>
|
||||||
{
|
{
|
||||||
@@ -257,7 +265,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
|
|
||||||
if (_cts.IsCancellationRequested)
|
if (_cts.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} stopped while starting");
|
_logger.OrderBookStoppedStarting(Api, Symbol);
|
||||||
await startResult.Data.CloseAsync().ConfigureAwait(false);
|
await startResult.Data.CloseAsync().ConfigureAwait(false);
|
||||||
Status = OrderBookStatus.Disconnected;
|
Status = OrderBookStatus.Disconnected;
|
||||||
return new CallResult<bool>(new CancellationRequestedError());
|
return new CallResult<bool>(new CancellationRequestedError());
|
||||||
@@ -272,16 +280,17 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
return new CallResult<bool>(true);
|
return new CallResult<bool>(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleConnectionLost() {
|
private void HandleConnectionLost()
|
||||||
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} connection lost");
|
{
|
||||||
if (Status != OrderBookStatus.Disposed) {
|
_logger.OrderBookConnectionLost(Api, Symbol);
|
||||||
|
if (Status != OrderBookStatus.Disposed) {
|
||||||
Status = OrderBookStatus.Reconnecting;
|
Status = OrderBookStatus.Reconnecting;
|
||||||
Reset();
|
Reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleConnectionClosed() {
|
private void HandleConnectionClosed() {
|
||||||
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} disconnected");
|
_logger.OrderBookDisconnected(Api, Symbol);
|
||||||
Status = OrderBookStatus.Disconnected;
|
Status = OrderBookStatus.Disconnected;
|
||||||
_ = StopAsync();
|
_ = StopAsync();
|
||||||
}
|
}
|
||||||
@@ -293,7 +302,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async Task StopAsync()
|
public async Task StopAsync()
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} stopping");
|
_logger.OrderBookStopping(Api, Symbol);
|
||||||
Status = OrderBookStatus.Disconnected;
|
Status = OrderBookStatus.Disconnected;
|
||||||
_cts?.Cancel();
|
_cts?.Cancel();
|
||||||
_queueEvent.Set();
|
_queueEvent.Set();
|
||||||
@@ -306,7 +315,8 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
_subscription.ConnectionClosed -= HandleConnectionClosed;
|
_subscription.ConnectionClosed -= HandleConnectionClosed;
|
||||||
_subscription.ConnectionRestored -= HandleConnectionRestored;
|
_subscription.ConnectionRestored -= HandleConnectionRestored;
|
||||||
}
|
}
|
||||||
_logger.Log(LogLevel.Trace, $"{Id} order book {Symbol} stopped");
|
|
||||||
|
_logger.OrderBookStopped(Api, Symbol);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -464,7 +474,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
{
|
{
|
||||||
var pbList = _processBuffer.ToList();
|
var pbList = _processBuffer.ToList();
|
||||||
if (pbList.Count > 0)
|
if (pbList.Count > 0)
|
||||||
_logger.Log(LogLevel.Debug, $"{Id} Processing {pbList.Count} buffered updates");
|
_logger.OrderBookProcessingBufferedUpdates(Api, Symbol, pbList.Count);
|
||||||
|
|
||||||
foreach (var bufferEntry in pbList)
|
foreach (var bufferEntry in pbList)
|
||||||
{
|
{
|
||||||
@@ -483,14 +493,14 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
{
|
{
|
||||||
if (sequence <= LastSequenceNumber)
|
if (sequence <= LastSequenceNumber)
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} update skipped #{sequence}, currently at #{LastSequenceNumber}");
|
_logger.OrderBookSkippedMessage(Api, Symbol, sequence, LastSequenceNumber);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_sequencesAreConsecutive && sequence > LastSequenceNumber + 1)
|
if (_sequencesAreConsecutive && sequence > LastSequenceNumber + 1)
|
||||||
{
|
{
|
||||||
// Out of sync
|
// Out of sync
|
||||||
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} out of sync (expected { LastSequenceNumber + 1}, was {sequence}), reconnecting");
|
_logger.OrderBookOutOfSync(Api, Symbol, LastSequenceNumber + 1, sequence);
|
||||||
_stopProcessing = true;
|
_stopProcessing = true;
|
||||||
Resubscribe();
|
Resubscribe();
|
||||||
return false;
|
return false;
|
||||||
@@ -644,7 +654,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
success = resyncResult;
|
success = resyncResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.Log(LogLevel.Information, $"{Id} order book {Symbol} successfully resynchronized");
|
_logger.OrderBookResynced(Api, Symbol);
|
||||||
Status = OrderBookStatus.Synced;
|
Status = OrderBookStatus.Synced;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -661,7 +671,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
|
|
||||||
if (_stopProcessing)
|
if (_stopProcessing)
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Trace, $"{Id} Skipping message because of resubscribing");
|
_logger.OrderBookMessageSkippedResubscribing(Api, Symbol);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -693,7 +703,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
BidCount = _bids.Count;
|
BidCount = _bids.Count;
|
||||||
|
|
||||||
UpdateTime = DateTime.UtcNow;
|
UpdateTime = DateTime.UtcNow;
|
||||||
_logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks. #{item.EndUpdateId}");
|
_logger.OrderBookDataSet(Api, Symbol, BidCount, AskCount, item.EndUpdateId);
|
||||||
CheckProcessBuffer();
|
CheckProcessBuffer();
|
||||||
OnOrderBookUpdate?.Invoke((item.Bids, item.Asks));
|
OnOrderBookUpdate?.Invoke((item.Bids, item.Asks));
|
||||||
OnBestOffersChanged?.Invoke((BestBid, BestAsk));
|
OnBestOffersChanged?.Invoke((BestBid, BestAsk));
|
||||||
@@ -713,7 +723,8 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
FirstUpdateId = item.StartUpdateId,
|
FirstUpdateId = item.StartUpdateId,
|
||||||
LastUpdateId = item.EndUpdateId,
|
LastUpdateId = item.EndUpdateId,
|
||||||
});
|
});
|
||||||
_logger.Log(LogLevel.Trace, $"{Id} order book {Symbol} update buffered #{item.StartUpdateId}-#{item.EndUpdateId} [{item.Asks.Count()} asks, {item.Bids.Count()} bids]");
|
|
||||||
|
_logger.OrderBookUpdateBuffered(Api, Symbol, item.StartUpdateId, item.EndUpdateId, item.Asks.Count(), item.Bids.Count());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -726,7 +737,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
|
|
||||||
if (_asks.First().Key < _bids.First().Key)
|
if (_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");
|
_logger.OrderBookOutOfSyncDetected(Api, Symbol, _asks.First().Key, _bids.First().Key);
|
||||||
_stopProcessing = true;
|
_stopProcessing = true;
|
||||||
Resubscribe();
|
Resubscribe();
|
||||||
return;
|
return;
|
||||||
@@ -760,7 +771,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
|
|
||||||
if (!checksumResult)
|
if (!checksumResult)
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} out of sync. Resyncing");
|
_logger.OrderBookOutOfSyncChecksum(Api, Symbol);
|
||||||
_stopProcessing = true;
|
_stopProcessing = true;
|
||||||
Resubscribe();
|
Resubscribe();
|
||||||
}
|
}
|
||||||
@@ -784,7 +795,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
if (!await _subscription!.ResubscribeAsync().ConfigureAwait(false))
|
if (!await _subscription!.ResubscribeAsync().ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
// Resubscribing failed, reconnect the socket
|
// Resubscribing failed, reconnect the socket
|
||||||
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} resync failed, reconnecting socket");
|
_logger.OrderBookResyncFailed(Api, Symbol);
|
||||||
Status = OrderBookStatus.Reconnecting;
|
Status = OrderBookStatus.Reconnecting;
|
||||||
_ = _subscription!.ReconnectAsync();
|
_ = _subscription!.ReconnectAsync();
|
||||||
}
|
}
|
||||||
@@ -799,7 +810,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
{
|
{
|
||||||
if (lastUpdateId <= LastSequenceNumber)
|
if (lastUpdateId <= LastSequenceNumber)
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Trace, $"{Id} order book {Symbol} update skipped #{firstUpdateId}-{lastUpdateId}");
|
_logger.OrderBookUpdateSkipped(Api, Symbol, firstUpdateId, lastUpdateId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -825,7 +836,8 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
}
|
}
|
||||||
|
|
||||||
LastSequenceNumber = lastUpdateId;
|
LastSequenceNumber = lastUpdateId;
|
||||||
_logger.Log(LogLevel.Trace, $"{Id} order book {Symbol} update processed #{firstUpdateId}-{lastUpdateId}");
|
|
||||||
|
_logger.OrderBookProcessedMessage(Api, Symbol, firstUpdateId, lastUpdateId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
|
using System.Security;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.RateLimiting.Filters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Filter requests based on whether they're authenticated or not
|
||||||
|
/// </summary>
|
||||||
|
public class AuthenticatedEndpointFilter : IGuardFilter
|
||||||
|
{
|
||||||
|
private readonly bool _authenticated;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="authenticated"></param>
|
||||||
|
public AuthenticatedEndpointFilter(bool authenticated)
|
||||||
|
{
|
||||||
|
_authenticated = authenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
|
||||||
|
=> definition.Authenticated == _authenticated;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Security;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.RateLimiting.Filters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Filter requests based on whether the request path matches a specific path
|
||||||
|
/// </summary>
|
||||||
|
public class ExactPathFilter : IGuardFilter
|
||||||
|
{
|
||||||
|
private readonly string _path;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path"></param>
|
||||||
|
public ExactPathFilter(string path)
|
||||||
|
{
|
||||||
|
_path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
|
||||||
|
=> string.Equals(definition.Path, _path, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Security;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.RateLimiting.Filters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Filter requests based on whether the request path matches any specific path in a list
|
||||||
|
/// </summary>
|
||||||
|
public class ExactPathsFilter : IGuardFilter
|
||||||
|
{
|
||||||
|
private readonly HashSet<string> _paths;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="paths"></param>
|
||||||
|
public ExactPathsFilter(IEnumerable<string> paths)
|
||||||
|
{
|
||||||
|
_paths = new HashSet<string>(paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
|
||||||
|
=> _paths.Contains(definition.Path);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
|
using System.Security;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.RateLimiting.Filters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Filter requests based on whether the host address matches a specific address
|
||||||
|
/// </summary>
|
||||||
|
public class HostFilter : IGuardFilter
|
||||||
|
{
|
||||||
|
private readonly string _host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="host"></param>
|
||||||
|
public HostFilter(string host)
|
||||||
|
{
|
||||||
|
_host = host;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
|
||||||
|
=> host == _host;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
|
using System.Security;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.RateLimiting.Filters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Filter requests based on whether it's a connection or a request
|
||||||
|
/// </summary>
|
||||||
|
public class LimitItemTypeFilter : IGuardFilter
|
||||||
|
{
|
||||||
|
private readonly RateLimitItemType _type;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type"></param>
|
||||||
|
public LimitItemTypeFilter(RateLimitItemType type)
|
||||||
|
{
|
||||||
|
_type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
|
||||||
|
=> type == _type;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Security;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.RateLimiting.Filters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Filter requests based on whether the path starts with a specific string
|
||||||
|
/// </summary>
|
||||||
|
public class PathStartFilter : IGuardFilter
|
||||||
|
{
|
||||||
|
private readonly string _path;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path"></param>
|
||||||
|
public PathStartFilter(string path)
|
||||||
|
{
|
||||||
|
_path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
|
||||||
|
=> definition.Path.StartsWith(_path, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Trackers;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Security;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.RateLimiting.Guards
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class RateLimitGuard : IRateLimitGuard
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Apply guard per host
|
||||||
|
/// </summary>
|
||||||
|
public static Func<RequestDefinition, string, SecureString?, string> PerHost { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => host);
|
||||||
|
/// <summary>
|
||||||
|
/// Apply guard per endpoint
|
||||||
|
/// </summary>
|
||||||
|
public static Func<RequestDefinition, string, SecureString?, string> PerEndpoint { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => def.Path + def.Method);
|
||||||
|
/// <summary>
|
||||||
|
/// Apply guard per API key
|
||||||
|
/// </summary>
|
||||||
|
public static Func<RequestDefinition, string, SecureString?, string> PerApiKey { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => key!.GetString());
|
||||||
|
/// <summary>
|
||||||
|
/// Apply guard per API key per endpoint
|
||||||
|
/// </summary>
|
||||||
|
public static Func<RequestDefinition, string, SecureString?, string> PerApiKeyPerEndpoint { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => key!.GetString() + def.Path + def.Method);
|
||||||
|
|
||||||
|
private readonly IEnumerable<IGuardFilter> _filters;
|
||||||
|
private readonly Dictionary<string, IWindowTracker> _trackers;
|
||||||
|
private RateLimitWindowType _windowType;
|
||||||
|
private double? _decayRate;
|
||||||
|
private int? _connectionWeight;
|
||||||
|
private readonly Func<RequestDefinition, string, SecureString?, string> _keySelector;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Name => "RateLimitGuard";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Description => _windowType == RateLimitWindowType.Decay ? $"Limit of {Limit} with a decay rate of {_decayRate}" : $"Limit of {Limit} per {TimeSpan}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The limit per period
|
||||||
|
/// </summary>
|
||||||
|
public int Limit { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// The time period for the limit
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan TimeSpan { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="keySelector">The rate limit key selector</param>
|
||||||
|
/// <param name="filter">Filter for rate limit items. Only when the rate limit item passes the filter the guard will apply</param>
|
||||||
|
/// <param name="limit">Limit per period</param>
|
||||||
|
/// <param name="timeSpan">Timespan for the period</param>
|
||||||
|
/// <param name="windowType">Type of rate limit window</param>
|
||||||
|
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
|
||||||
|
/// <param name="connectionWeight">The weight of a new connection</param>
|
||||||
|
public RateLimitGuard(Func<RequestDefinition, string, SecureString?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
|
||||||
|
: this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="keySelector">The rate limit key selector</param>
|
||||||
|
/// <param name="filters">Filters for rate limit items. Only when the rate limit item passes all filters the guard will apply</param>
|
||||||
|
/// <param name="limit">Limit per period</param>
|
||||||
|
/// <param name="timeSpan">Timespan for the period</param>
|
||||||
|
/// <param name="windowType">Type of rate limit window</param>
|
||||||
|
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
|
||||||
|
/// <param name="connectionWeight">The weight of a new connection</param>
|
||||||
|
public RateLimitGuard(Func<RequestDefinition, string, SecureString?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
|
||||||
|
{
|
||||||
|
_filters = filters;
|
||||||
|
_trackers = new Dictionary<string, IWindowTracker>();
|
||||||
|
_windowType = windowType;
|
||||||
|
Limit = limit;
|
||||||
|
TimeSpan = timeSpan;
|
||||||
|
_keySelector = keySelector;
|
||||||
|
_decayRate = decayPerTimeSpan;
|
||||||
|
_connectionWeight = connectionWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
||||||
|
{
|
||||||
|
foreach(var filter in _filters)
|
||||||
|
{
|
||||||
|
if (!filter.Passes(type, definition, host, apiKey))
|
||||||
|
return LimitCheck.NotApplicable;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == RateLimitItemType.Connection)
|
||||||
|
requestWeight = _connectionWeight ?? requestWeight;
|
||||||
|
|
||||||
|
var key = _keySelector(definition, host, apiKey);
|
||||||
|
if (!_trackers.TryGetValue(key, out var tracker))
|
||||||
|
{
|
||||||
|
tracker = CreateTracker();
|
||||||
|
_trackers.Add(key, tracker);
|
||||||
|
}
|
||||||
|
|
||||||
|
var delay = tracker.GetWaitTime(requestWeight);
|
||||||
|
if (delay == default)
|
||||||
|
return LimitCheck.NotNeeded;
|
||||||
|
|
||||||
|
return LimitCheck.Needed(delay, Limit, TimeSpan, tracker.Current);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
||||||
|
{
|
||||||
|
foreach (var filter in _filters)
|
||||||
|
{
|
||||||
|
if (!filter.Passes(type, definition, host, apiKey))
|
||||||
|
return RateLimitState.NotApplied;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == RateLimitItemType.Connection)
|
||||||
|
requestWeight = _connectionWeight ?? requestWeight;
|
||||||
|
|
||||||
|
var key = _keySelector(definition, host, apiKey);
|
||||||
|
var tracker = _trackers[key];
|
||||||
|
tracker.ApplyWeight(requestWeight);
|
||||||
|
return RateLimitState.Applied(Limit, TimeSpan, tracker.Current);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new WindowTracker
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected IWindowTracker CreateTracker()
|
||||||
|
{
|
||||||
|
return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(Limit, TimeSpan)
|
||||||
|
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(Limit, TimeSpan)
|
||||||
|
: _windowType == RateLimitWindowType.FixedAfterFirst ? new FixedAfterStartWindowTracker(Limit, TimeSpan) :
|
||||||
|
new DecayWindowTracker(Limit, TimeSpan, _decayRate ?? throw new InvalidOperationException("Decay rate not provided"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Security;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.RateLimiting.Guards
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retry after guard
|
||||||
|
/// </summary>
|
||||||
|
public class RetryAfterGuard : IRateLimitGuard
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Additional wait time to apply to account for time offset between server and client
|
||||||
|
/// </summary>
|
||||||
|
private static readonly TimeSpan _windowBuffer = TimeSpan.FromMilliseconds(1000);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Name => "RetryAfterGuard";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Description => $"Pause requests until after {After}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The timestamp after which requests are allowed again
|
||||||
|
/// </summary>
|
||||||
|
public DateTime After { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="after"></param>
|
||||||
|
public RetryAfterGuard(DateTime after)
|
||||||
|
{
|
||||||
|
After = after;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
||||||
|
{
|
||||||
|
var dif = (After + _windowBuffer) - DateTime.UtcNow;
|
||||||
|
if (dif <= TimeSpan.Zero)
|
||||||
|
return LimitCheck.NotApplicable;
|
||||||
|
|
||||||
|
return LimitCheck.Needed(dif, default, default, default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
||||||
|
{
|
||||||
|
return RateLimitState.NotApplied;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update the 'after' time
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="after"></param>
|
||||||
|
public void UpdateAfter(DateTime after) => After = after;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Trackers;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Security;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.RateLimiting.Guards
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Rate limit guard for a per endpoint limit
|
||||||
|
/// </summary>
|
||||||
|
public class SingleLimitGuard : IRateLimitGuard
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, IWindowTracker> _trackers;
|
||||||
|
private readonly RateLimitWindowType _windowType;
|
||||||
|
private readonly double? _decayRate;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Name => "EndpointLimitGuard";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Description => $"Limit requests to endpoint";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public SingleLimitGuard(RateLimitWindowType windowType, double? decayRate = null)
|
||||||
|
{
|
||||||
|
_windowType = windowType;
|
||||||
|
_decayRate = decayRate;
|
||||||
|
_trackers = new Dictionary<string, IWindowTracker>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
||||||
|
{
|
||||||
|
var key = definition.Path + definition.Method;
|
||||||
|
if (!_trackers.TryGetValue(key, out var tracker))
|
||||||
|
{
|
||||||
|
tracker = CreateTracker(definition.EndpointLimitCount!.Value, definition.EndpointLimitPeriod!.Value);
|
||||||
|
_trackers.Add(key, tracker);
|
||||||
|
}
|
||||||
|
|
||||||
|
var delay = tracker.GetWaitTime(requestWeight);
|
||||||
|
if (delay == default)
|
||||||
|
return LimitCheck.NotNeeded;
|
||||||
|
|
||||||
|
return LimitCheck.Needed(delay, definition.EndpointLimitCount!.Value, definition.EndpointLimitPeriod!.Value, tracker.Current);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
||||||
|
{
|
||||||
|
var key = definition.Path + definition.Method;
|
||||||
|
var tracker = _trackers[key];
|
||||||
|
tracker.ApplyWeight(requestWeight);
|
||||||
|
return RateLimitState.Applied(definition.EndpointLimitCount!.Value, definition.EndpointLimitPeriod!.Value, tracker.Current);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new WindowTracker
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected IWindowTracker CreateTracker(int limit, TimeSpan timeSpan)
|
||||||
|
{
|
||||||
|
return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(limit, timeSpan)
|
||||||
|
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(limit, timeSpan) :
|
||||||
|
new DecayWindowTracker(limit, timeSpan, _decayRate ?? throw new InvalidOperationException("Decay rate not provided"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System.Security;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Filter requests based on specific condition
|
||||||
|
/// </summary>
|
||||||
|
public interface IGuardFilter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether a request or connection passes this filter
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The type of item</param>
|
||||||
|
/// <param name="definition">The request definition</param>
|
||||||
|
/// <param name="host">The host address</param>
|
||||||
|
/// <param name="apiKey">The API key</param>
|
||||||
|
/// <returns>True if passed</returns>
|
||||||
|
bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Guards;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Security;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Rate limit gate
|
||||||
|
/// </summary>
|
||||||
|
public interface IRateLimitGate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Event when the rate limit is triggered
|
||||||
|
/// </summary>
|
||||||
|
event Action<RateLimitEvent> RateLimitTriggered;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a rate limit guard
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="guard">Guard to add</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IRateLimitGate AddGuard(IRateLimitGuard guard);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set a RetryAfter guard, can be used when a server rate limit is hit and a RetryAfter header is specified
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="retryAfter">The time after which requests can be send again</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task SetRetryAfterGuardAsync(DateTime retryAfter);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the SingleLimitGuard for handling individual endpoint rate limits
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="guard"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IRateLimitGate SetSingleLimitGuard(SingleLimitGuard guard);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the 'retry after' timestamp if set
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<DateTime?> GetRetryAfterTime();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process a request. Enforces the configured rate limits. When a rate limit is hit will wait for the rate limit to pass if RateLimitingBehaviour is Wait, or return an error if it is set to Fail
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Logger</param>
|
||||||
|
/// <param name="itemId">Id of the item to check</param>
|
||||||
|
/// <param name="type">The rate limit item type</param>
|
||||||
|
/// <param name="definition">The request definition</param>
|
||||||
|
/// <param name="baseAddress">The host address</param>
|
||||||
|
/// <param name="apiKey">The API key</param>
|
||||||
|
/// <param name="requestWeight">Request weight</param>
|
||||||
|
/// <param name="behaviour">Behaviour when rate limit is hit</param>
|
||||||
|
/// <param name="ct">Cancelation token</param>
|
||||||
|
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
|
||||||
|
Task<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, SecureString? apiKey, int requestWeight, RateLimitingBehaviour behaviour, CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enforces the rate limit as defined in the request definition. When a rate limit is hit will wait for the rate limit to pass if RateLimitingBehaviour is Wait, or return an error if it is set to Fail
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Logger</param>
|
||||||
|
/// <param name="itemId">Id of the item to check</param>
|
||||||
|
/// <param name="type">The rate limit item type</param>
|
||||||
|
/// <param name="definition">The request definition</param>
|
||||||
|
/// <param name="baseAddress">The host address</param>
|
||||||
|
/// <param name="apiKey">The API key</param>
|
||||||
|
/// <param name="requestWeight">Request weight</param>
|
||||||
|
/// <param name="behaviour">Behaviour when rate limit is hit</param>
|
||||||
|
/// <param name="ct">Cancelation token</param>
|
||||||
|
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
|
||||||
|
Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, SecureString? apiKey, int requestWeight, RateLimitingBehaviour behaviour, CancellationToken ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Security;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Rate limit guard
|
||||||
|
/// </summary>
|
||||||
|
public interface IRateLimitGuard
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Name
|
||||||
|
/// </summary>
|
||||||
|
string Name { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Description
|
||||||
|
/// </summary>
|
||||||
|
string Description { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check whether a request can pass this rate limit guard
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The rate limit item type</param>
|
||||||
|
/// <param name="definition">The request definition</param>
|
||||||
|
/// <param name="host">The host address</param>
|
||||||
|
/// <param name="apiKey">The API key</param>
|
||||||
|
/// <param name="requestWeight">The request weight</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Apply the request to this guard with the specified weight
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The rate limit item type</param>
|
||||||
|
/// <param name="definition">The request definition</param>
|
||||||
|
/// <param name="host">The host address</param>
|
||||||
|
/// <param name="apiKey">The API key</param>
|
||||||
|
/// <param name="requestWeight">The request weight</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Rate limit window tracker
|
||||||
|
/// </summary>
|
||||||
|
public interface IWindowTracker
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Time period the limit is for
|
||||||
|
/// </summary>
|
||||||
|
TimeSpan TimePeriod { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// The limit in the time period
|
||||||
|
/// </summary>
|
||||||
|
int Limit { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// The current count within the time period
|
||||||
|
/// </summary>
|
||||||
|
int Current { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Get the time to wait to fit the weight
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="weight"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
TimeSpan GetWaitTime(int weight);
|
||||||
|
/// <summary>
|
||||||
|
/// Register the weight in this window
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="weight">Request weight</param>
|
||||||
|
void ApplyWeight(int weight);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.RateLimiting
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Limit check
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct LimitCheck
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Is guard applicable
|
||||||
|
/// </summary>
|
||||||
|
public bool Applicable { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Delay needed
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan Delay { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Current counter
|
||||||
|
/// </summary>
|
||||||
|
public int Current { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Limit
|
||||||
|
/// </summary>
|
||||||
|
public int? Limit { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Time period
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? Period { get; }
|
||||||
|
|
||||||
|
private LimitCheck(bool applicable, TimeSpan delay, int limit, TimeSpan period, int current)
|
||||||
|
{
|
||||||
|
Applicable = applicable;
|
||||||
|
Delay = delay;
|
||||||
|
Limit = limit;
|
||||||
|
Period = period;
|
||||||
|
Current = current;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Not applicable
|
||||||
|
/// </summary>
|
||||||
|
public static LimitCheck NotApplicable { get; } = new LimitCheck(false, default, default, default, default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// No wait needed
|
||||||
|
/// </summary>
|
||||||
|
public static LimitCheck NotNeeded { get; } = new LimitCheck(true, default, default, default, default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wait needed
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="delay">The delay needed</param>
|
||||||
|
/// <param name="limit">Limit per period</param>
|
||||||
|
/// <param name="period">Period the limit is for</param>
|
||||||
|
/// <param name="current">Current counter</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static LimitCheck Needed(TimeSpan delay, int limit, TimeSpan period, int current) => new(true, delay, limit, period, current);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.RateLimiting
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A rate limit entry
|
||||||
|
/// </summary>
|
||||||
|
public struct LimitEntry
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Timestamp of the item
|
||||||
|
/// </summary>
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Item weight
|
||||||
|
/// </summary>
|
||||||
|
public int Weight { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="timestamp"></param>
|
||||||
|
/// <param name="weight"></param>
|
||||||
|
public LimitEntry(DateTime timestamp, int weight)
|
||||||
|
{
|
||||||
|
Timestamp = timestamp;
|
||||||
|
Weight = weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.RateLimiting
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Rate limit event
|
||||||
|
/// </summary>
|
||||||
|
public record RateLimitEvent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Name of the API limit that is reached
|
||||||
|
/// </summary>
|
||||||
|
public string ApiLimit { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Description of the limit that is reached
|
||||||
|
/// </summary>
|
||||||
|
public string LimitDescription { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// The request definition
|
||||||
|
/// </summary>
|
||||||
|
public RequestDefinition RequestDefinition { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The host the request is for
|
||||||
|
/// </summary>
|
||||||
|
public string Host { get; set; } = default!;
|
||||||
|
/// <summary>
|
||||||
|
/// The current counter value
|
||||||
|
/// </summary>
|
||||||
|
public int Current { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The weight of the limited request
|
||||||
|
/// </summary>
|
||||||
|
public int RequestWeight { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The limit per time period
|
||||||
|
/// </summary>
|
||||||
|
public int? Limit { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The time period the limit is for
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? TimePeriod { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The time the request will be delayed for if the Behaviour is RateLimitingBehaviour.Wait
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? DelayTime { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The handling behaviour for the rquest
|
||||||
|
/// </summary>
|
||||||
|
public RateLimitingBehaviour Behaviour { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="apiLimit"></param>
|
||||||
|
/// <param name="limitDescription"></param>
|
||||||
|
/// <param name="definition"></param>
|
||||||
|
/// <param name="host"></param>
|
||||||
|
/// <param name="current"></param>
|
||||||
|
/// <param name="requestWeight"></param>
|
||||||
|
/// <param name="limit"></param>
|
||||||
|
/// <param name="timePeriod"></param>
|
||||||
|
/// <param name="delayTime"></param>
|
||||||
|
/// <param name="behaviour"></param>
|
||||||
|
public RateLimitEvent(string apiLimit, string limitDescription, RequestDefinition definition, string host, int current, int requestWeight, int? limit, TimeSpan? timePeriod, TimeSpan? delayTime, RateLimitingBehaviour behaviour)
|
||||||
|
{
|
||||||
|
ApiLimit = apiLimit;
|
||||||
|
LimitDescription = limitDescription;
|
||||||
|
RequestDefinition = definition;
|
||||||
|
Host = host;
|
||||||
|
Current = current;
|
||||||
|
RequestWeight = requestWeight;
|
||||||
|
Limit = limit;
|
||||||
|
TimePeriod = timePeriod;
|
||||||
|
DelayTime = delayTime;
|
||||||
|
Behaviour = behaviour;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
using CryptoExchange.Net.Logging.Extensions;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Guards;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.RateLimiting
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class RateLimitGate : IRateLimitGate
|
||||||
|
{
|
||||||
|
private IRateLimitGuard _singleLimitGuard = new SingleLimitGuard(RateLimitWindowType.Sliding);
|
||||||
|
private readonly ConcurrentBag<IRateLimitGuard> _guards;
|
||||||
|
private readonly SemaphoreSlim _semaphore;
|
||||||
|
private readonly string _name;
|
||||||
|
|
||||||
|
private int _waitingCount;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public event Action<RateLimitEvent>? RateLimitTriggered;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public RateLimitGate(string name)
|
||||||
|
{
|
||||||
|
_name = name;
|
||||||
|
_guards = new ConcurrentBag<IRateLimitGuard>();
|
||||||
|
_semaphore = new SemaphoreSlim(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||||
|
_waitingCount++;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await CheckGuardsAsync(_guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_waitingCount--;
|
||||||
|
_semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||||
|
if (requestWeight == 0)
|
||||||
|
requestWeight = 1;
|
||||||
|
|
||||||
|
_waitingCount++;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await CheckGuardsAsync(new IRateLimitGuard[] { _singleLimitGuard }, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_waitingCount--;
|
||||||
|
_semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<CallResult> CheckGuardsAsync(IEnumerable<IRateLimitGuard> guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
|
||||||
|
{
|
||||||
|
foreach (var guard in guards)
|
||||||
|
{
|
||||||
|
// Check if a wait is needed for this guard
|
||||||
|
var result = guard.Check(type, definition, host, apiKey, requestWeight);
|
||||||
|
if (result.Delay != TimeSpan.Zero && rateLimitingBehaviour == RateLimitingBehaviour.Fail)
|
||||||
|
{
|
||||||
|
// Delay is needed and limit behaviour is to fail the request
|
||||||
|
if (type == RateLimitItemType.Connection)
|
||||||
|
logger.RateLimitConnectionFailed(itemId, guard.Name, guard.Description);
|
||||||
|
else
|
||||||
|
logger.RateLimitRequestFailed(itemId, definition.Path, guard.Name, guard.Description);
|
||||||
|
|
||||||
|
RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
|
||||||
|
return new CallResult(new ClientRateLimitError($"Rate limit check failed on guard {guard.Name}; {guard.Description}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Delay != TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
// Delay is needed and limit behaviour is to wait for the request to be under the limit
|
||||||
|
_semaphore.Release();
|
||||||
|
|
||||||
|
var description = result.Limit == null ? guard.Description : $"{guard.Description}, Request weight: {requestWeight}, Current: {result.Current}, Limit: {result.Limit}, requests now being limited: {_waitingCount}";
|
||||||
|
if (type == RateLimitItemType.Connection)
|
||||||
|
logger.RateLimitDelayingConnection(itemId, result.Delay, guard.Name, description);
|
||||||
|
else
|
||||||
|
logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description);
|
||||||
|
|
||||||
|
RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
|
||||||
|
await Task.Delay(result.Delay, ct).ConfigureAwait(false);
|
||||||
|
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||||
|
return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the weight on each guard
|
||||||
|
foreach (var guard in guards)
|
||||||
|
{
|
||||||
|
var result = guard.ApplyWeight(type, definition, host, apiKey, requestWeight);
|
||||||
|
if (result.IsApplied)
|
||||||
|
{
|
||||||
|
if (type == RateLimitItemType.Connection)
|
||||||
|
logger.RateLimitAppliedConnection(itemId, guard.Name, guard.Description, result.Current);
|
||||||
|
else
|
||||||
|
logger.RateLimitAppliedRequest(itemId, definition.Path, guard.Name, guard.Description, result.Current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CallResult(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IRateLimitGate AddGuard(IRateLimitGuard guard)
|
||||||
|
{
|
||||||
|
_guards.Add(guard);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IRateLimitGate SetSingleLimitGuard(SingleLimitGuard guard)
|
||||||
|
{
|
||||||
|
_singleLimitGuard = guard;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task SetRetryAfterGuardAsync(DateTime retryAfter)
|
||||||
|
{
|
||||||
|
await _semaphore.WaitAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var retryAfterGuard = _guards.OfType<RetryAfterGuard>().SingleOrDefault();
|
||||||
|
if (retryAfterGuard == null)
|
||||||
|
_guards.Add(new RetryAfterGuard(retryAfter));
|
||||||
|
else
|
||||||
|
retryAfterGuard.UpdateAfter(retryAfter);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<DateTime?> GetRetryAfterTime()
|
||||||
|
{
|
||||||
|
await _semaphore.WaitAsync().ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var retryAfterGuard = _guards.OfType<RetryAfterGuard>().SingleOrDefault();
|
||||||
|
return retryAfterGuard?.After;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user