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

Compare commits

..

113 Commits

Author SHA1 Message Date
JKorf 30e100d01a Updated version 2024-02-21 19:42:56 +01:00
JKorf 4f79c0ed10 Fixed example 2024-02-21 19:42:09 +01:00
JKorf f917bf0e3f Updated revitalize request signature, added exception handler reconnect logic 2024-02-21 19:39:31 +01:00
JKorf 68e525ab9d Logging 2024-02-20 22:06:17 +01:00
JKorf 571dc26d5d Added icon and readme 2024-02-20 16:56:23 +01:00
JKorf 63249a99db Update index.html 2024-02-15 21:26:54 +01:00
JKorf e1c5c3d19f Update index.html 2024-02-14 21:34:51 +01:00
JKorf f33857768b Update index.html 2024-02-14 21:24:10 +01:00
JKorf 46da3b40a0 docs 2024-02-14 16:32:48 +01:00
JKorf bbad338941 Update logo.png 2024-02-13 21:45:44 +01:00
JKorf 3e290b185e Docs 2024-02-13 21:37:25 +01:00
JKorf bdbec48155 Docs 2024-02-13 17:03:06 +01:00
JKorf 76e95cf870 Docs and examples update 2024-02-11 16:47:48 +01:00
JKorf da1686d95c docs 2024-02-09 19:55:26 +01:00
JKorf 22cd2268ab wip 2024-02-08 21:59:53 +01:00
JKorf 8d45b4b069 docs 2024-02-08 17:51:49 +01:00
JKorf ae4a8bdc32 docs 2024-02-07 21:46:51 +01:00
JKorf 764a56ec7f docs 2024-02-07 16:26:57 +01:00
JKorf ad28d1e00e Update index.html 2024-02-07 16:14:36 +01:00
JKorf 1c5e9b0d64 Test 2024-02-07 16:09:57 +01:00
JKorf 7ac75b9abe Fixed version error 2024-02-06 20:30:59 +01:00
JKorf d2dc8a06e5 Updated version 2024-02-06 19:31:23 +01:00
JKorf 9ddd446892 Some adjustment to the Crypto clients 2024-02-06 19:28:31 +01:00
JKorf 1eb0214e7a Wip docs 2024-02-06 16:26:42 +01:00
JKorf 4e4ffcab1c Corrected ICryptoRestClient and ICryptoSocketClient namespaces 2024-02-06 16:25:27 +01:00
JKorf 0d6a100aac Update README.md 2024-02-06 14:55:26 +01:00
JKorf 99df7cc792 comments 2024-02-04 17:42:10 +01:00
JKorf 3fa8277a30 Small refactor QueryPeriodic to only run on connections 2024-02-04 13:50:15 +01:00
JKorf d0fc67355d wip 2024-02-03 15:35:27 +01:00
JKorf 8a869e8e1d wip 2024-02-03 15:35:27 +01:00
JKorf acd9b0d533 wip + tests 2024-02-03 15:35:27 +01:00
JKorf fc6503035a Cleanup 2024-02-03 15:35:27 +01:00
JKorf e3207033c3 wip 2024-02-03 15:35:27 +01:00
JKorf b057974cd0 wip 2024-02-03 15:35:27 +01:00
JKorf 9ead87d350 wip 2024-02-03 15:35:27 +01:00
JKorf c1ee36dd8a wip 2024-02-03 15:35:27 +01:00
JKorf eee19b28a5 wip 2024-02-03 15:35:27 +01:00
JKorf 58098edaa6 wip 2024-02-03 15:35:27 +01:00
JKorf c931a60cb7 wip 2024-02-03 15:35:27 +01:00
JKorf 12d5783625 wip 2024-02-03 15:35:27 +01:00
JKorf b640690a0f wip 2024-02-03 15:35:27 +01:00
JKorf c41e128900 wip 2024-02-03 15:35:27 +01:00
JKorf 1ba66be29f wip 2024-02-03 15:35:27 +01:00
JKorf ff6a9d5f13 wip 2024-02-03 15:35:26 +01:00
JKorf b59fe9e3ef wip 2024-02-03 15:35:26 +01:00
JKorf ac434fa2c6 wip 2024-02-03 15:35:26 +01:00
JKorf 3de04e4828 wip 2024-02-03 15:35:26 +01:00
JKorf 081c2d4268 wip 2024-02-03 15:35:26 +01:00
JKorf 6fa66d819d wip 2024-02-03 15:35:26 +01:00
JKorf 312d54cf04 wip 2024-02-03 15:35:26 +01:00
JKorf 35f7dbf9fb wip 2024-02-03 15:35:26 +01:00
JKorf 5539320827 wip 2024-02-03 15:35:26 +01:00
JKorf cf941fe5c9 wip 2024-02-03 15:35:26 +01:00
JKorf ad3959a8e9 wip 2024-02-03 15:35:26 +01:00
JKorf 9f92d86855 wip 2024-02-03 15:35:26 +01:00
JKorf bee2e86c2f wip 2024-02-03 15:35:26 +01:00
JKorf bf854c92af wip 2024-02-03 15:35:26 +01:00
JKorf 141d5bd956 wip 2024-02-03 15:35:26 +01:00
JKorf cff3863373 wip 2024-02-03 15:35:26 +01:00
JKorf d91755dff5 Updated version 2024-01-09 18:37:11 +01:00
JKorf 27d49a6093 Added support null and empty string values to BoolConverter 2024-01-09 18:36:20 +01:00
JKorf ad1bdd9a3f Fixed error 2024-01-04 21:33:10 +01:00
JKorf 4f2d7abc7e Updated version 2024-01-04 21:32:27 +01:00
JKorf ef9de5e338 Fixed parsing of string datetime value "0.00000" 2024-01-04 21:31:51 +01:00
JKorf 8b513e51b9 Updated version 2023-12-02 15:17:34 +01:00
JKorf d43b38a23a Fix requestBodyFormat parameter usage 2023-12-02 15:15:38 +01:00
JKorf 0987c0f9d1 Updated version 2023-12-02 14:22:12 +01:00
JKorf e2dde77023 Added DecimalStringWriter json converter, added support for specifying body content type on a per request basis 2023-12-02 14:20:41 +01:00
JKorf 104ac7caad Updated response logging, added RequestId to WebCallResult 2023-11-30 14:19:58 +01:00
JKorf 8788dd3deb Updated version 2023-10-28 15:19:11 +02:00
JKorf f64cc5e9cf Added additional helpers methods 2023-10-28 15:17:10 +02:00
JKorf 75d1bbc6e8 Updated examples package versions 2023-10-24 21:11:50 +02:00
JKorf b621aa7e65 Updated version 2023-10-24 18:44:22 +02:00
JKorf 9783108695 Added support for writing ints to EnumConverter 2023-10-24 18:41:38 +02:00
JKorf 6ba32fe280 Removed some things for internal use from interfaces 2023-10-12 22:25:25 +02:00
JKorf f75cc75bbc Added SerializerOptions helper class for setting default serializer, Added ParameterCollection for easier parameter definition, Added extra encryption helper methods on AuthenticationProvider 2023-10-12 22:03:09 +02:00
JKorf 2109b65a8e Fixed incorrect options docs 2023-10-09 21:36:12 +02:00
JKorf a472751638 Updated Examples 2023-10-09 20:43:08 +02:00
JKorf f08ed16f2a Updated version 2023-10-08 17:01:35 +02:00
JKorf 212d457a6a Added UpdateType to DataEvent model, added additional scenarios to BoolConverter, updated some logging 2023-10-08 16:59:43 +02:00
JKorf ac5f333766 Updated version 2023-09-23 21:16:09 +02:00
JKorf 640e4387c1 Added BoolConverter, added parameter for showing warning message to EnumConverter 2023-09-23 21:13:49 +02:00
JKorf a16b19019f Updated version 2023-09-18 20:13:00 +02:00
JKorf 2443f576ac Fix for concurrency exception 2023-09-18 20:02:01 +02:00
JKorf 4fd7e44015 Logging 2023-09-16 18:26:10 +02:00
JKorf a0a3bda1c5 Updated version 2023-09-11 20:59:44 +02:00
JKorf 6bda7a3c73 Fixed nullreference if no Retry-After is returned after with a ratelimit error 2023-09-11 20:57:46 +02:00
JKorf 69a7a714cd Updated rate limiters to support multiple instances 2023-09-11 20:16:50 +02:00
JKorf 48e2e6468e Updated version 2023-09-04 18:10:18 +02:00
JKorf 4017ac780f ArrayConverter update for handling exponent notation, EnumConverter fix for writing enum values 2023-09-04 18:09:01 +02:00
JKorf a55cd1bb13 Docs 2023-08-26 20:04:38 +02:00
JKorf b34129e148 Updated version 2023-08-24 21:25:37 +02:00
JKorf be25a68c9c Ratelimiting for socket requests 2023-08-24 20:51:17 +02:00
JKorf 468cd5e48e Added RetryAfter property for ratelimit errors, added parsing of rate limit return 2023-08-21 21:34:26 +02:00
JKorf 262c4e4aa5 Dont process unsubscribe if there are no subscriptions 2023-08-21 20:11:31 +02:00
Jan Korf 4ccff6461f Merge pull request #177 from ASolomatin/master
Ability for all Error derived classes to have Code and Data
2023-08-21 20:08:50 +02:00
JKorf 3bfa3ef389 Update index.md 2023-08-04 22:50:17 +02:00
JKorf 5238971bcc Update index.md 2023-08-04 22:44:09 +02:00
JKorf 2f5c904faf Added okx 2023-08-04 22:38:21 +02:00
JKorf c62775813f Updated version 2023-07-23 13:52:07 +02:00
JKorf f11b3754f0 Fix for proxy when not using DI 2023-07-23 10:01:13 +02:00
JKorf 3cbe0465e9 Docs 2023-07-11 21:38:48 +02:00
JKorf 5048aea722 Updated version 2023-07-05 21:58:35 +02:00
Aleksej Solomatin 8d35339ab2 Ability for all Error derived classes to have Code and Data
Proposal https://github.com/JKorf/CryptoExchange.Net/issues/176
2023-07-05 19:39:04 +03:00
Jkorf c3316a51e7 Added properties dictionary to socket connection 2023-07-05 17:10:43 +02:00
JKorf 7ecf37064b Updated version 2023-06-29 20:10:35 +02:00
JKorf 18954f4f53 Added optional log level parameter for trace logger 2023-06-29 20:07:26 +02:00
JKorf 00bc245102 Updated examples 2023-06-26 20:45:28 +02:00
JKorf 273cab9fdb Docs 2023-06-25 21:43:30 +02:00
JKorf 84d0f0ec9e Updated version 2023-06-25 20:25:02 +02:00
JKorf 690f2a63e5 Squashed commit of the following:
commit 90f285d7f6bcd926ce9ca3d5832b1d70a5eae6ab
Author: JKorf <jankorf91@gmail.com>
Date:   Sun Jun 25 19:51:12 2023 +0200

    Docs

commit 72187035c703d1402b37bd2f4c3e066706f28d67
Author: JKorf <jankorf91@gmail.com>
Date:   Sat Jun 24 16:02:53 2023 +0200

    docs

commit 8411977292f1fb0b6e0705b1ad675b79a5311d90
Author: JKorf <jankorf91@gmail.com>
Date:   Fri Jun 23 18:25:15 2023 +0200

    wip

commit cb7d33aad5d2751104c8b8a6c6eadbf0d36b672c
Author: JKorf <jankorf91@gmail.com>
Date:   Fri Jun 2 19:26:26 2023 +0200

    wip

commit 4359a2d05ea1141cff516dab18f364a6ca854e18
Author: JKorf <jankorf91@gmail.com>
Date:   Wed May 31 20:51:36 2023 +0200

    wip

commit c6adb1b2f728d143f6bd667139c619581122a3c9
Author: JKorf <jankorf91@gmail.com>
Date:   Mon May 1 21:13:47 2023 +0200

    wip

commit 7fee733f82fa6ff574030452f0955c9e817647dd
Author: JKorf <jankorf91@gmail.com>
Date:   Thu Apr 27 13:02:56 2023 +0200

    wip

commit f8057313ffc9b0c31effcda71d35d105ea390971
Author: JKorf <jankorf91@gmail.com>
Date:   Mon Apr 17 21:37:51 2023 +0200

    wip
2023-06-25 19:58:46 +02:00
JKorf 19cc020852 Updated version 2023-04-14 21:03:36 +02:00
JKorf 1d4353e6d1 Fixed potential collection modified exception 2023-04-14 21:02:21 +02:00
629 changed files with 117458 additions and 4055 deletions
-2
View File
@@ -287,5 +287,3 @@ __pycache__/
*.odx.cs
*.xsd.cs
CryptoExchange.Net/CryptoExchange.Net.xml
/Docs/*
Docs/
@@ -11,66 +11,6 @@ namespace CryptoExchange.Net.UnitTests
[TestFixture()]
public class BaseClientTests
{
[TestCase]
public void SettingLogOutput_Should_RedirectLogOutput()
{
// arrange
var logger = new TestStringLogger();
var client = new TestBaseClient(new TestOptions()
{
LogWriters = new List<ILogger> { logger }
});
// act
client.Log(LogLevel.Information, "Test");
// assert
Assert.IsFalse(string.IsNullOrEmpty(logger.GetLogs()));
}
[TestCase(LogLevel.None, LogLevel.Error, false)]
[TestCase(LogLevel.None, LogLevel.Warning, false)]
[TestCase(LogLevel.None, LogLevel.Information, false)]
[TestCase(LogLevel.None, LogLevel.Debug, false)]
[TestCase(LogLevel.Error, LogLevel.Error, true)]
[TestCase(LogLevel.Error, LogLevel.Warning, false)]
[TestCase(LogLevel.Error, LogLevel.Information, false)]
[TestCase(LogLevel.Error, LogLevel.Debug, false)]
[TestCase(LogLevel.Warning, LogLevel.Error, true)]
[TestCase(LogLevel.Warning, LogLevel.Warning, true)]
[TestCase(LogLevel.Warning, LogLevel.Information, false)]
[TestCase(LogLevel.Warning, LogLevel.Debug, false)]
[TestCase(LogLevel.Information, LogLevel.Error, true)]
[TestCase(LogLevel.Information, LogLevel.Warning, true)]
[TestCase(LogLevel.Information, LogLevel.Information, true)]
[TestCase(LogLevel.Information, LogLevel.Debug, false)]
[TestCase(LogLevel.Debug, LogLevel.Error, true)]
[TestCase(LogLevel.Debug, LogLevel.Warning, true)]
[TestCase(LogLevel.Debug, LogLevel.Information, true)]
[TestCase(LogLevel.Debug, LogLevel.Debug, true)]
[TestCase(null, LogLevel.Error, true)]
[TestCase(null, LogLevel.Warning, true)]
[TestCase(null, LogLevel.Information, true)]
[TestCase(null, LogLevel.Debug, false)]
public void SettingLogLevel_Should_RestrictLogging(LogLevel? verbosity, LogLevel testVerbosity, bool expected)
{
// arrange
var logger = new TestStringLogger();
var options = new TestOptions()
{
LogWriters = new List<ILogger> { logger }
};
if (verbosity != null)
options.LogLevel = verbosity.Value;
var client = new TestBaseClient(options);
// act
client.Log(testVerbosity, "Test");
// assert
Assert.AreEqual(!string.IsNullOrEmpty(logger.GetLogs()), expected);
}
[TestCase]
public void DeserializingValidJson_Should_GiveSuccessfulResult()
{
@@ -113,7 +113,9 @@ namespace CryptoExchange.Net.UnitTests
System.Net.HttpStatusCode.OK,
new List<KeyValuePair<string, IEnumerable<string>>>(),
TimeSpan.FromSeconds(1),
null,
"{}",
1,
"https://test.com/api",
null,
HttpMethod.Get,
@@ -140,7 +142,9 @@ namespace CryptoExchange.Net.UnitTests
System.Net.HttpStatusCode.OK,
new List<KeyValuePair<string, IEnumerable<string>>>(),
TimeSpan.FromSeconds(1),
null,
"{}",
1,
"https://test.com/api",
null,
HttpMethod.Get,
@@ -20,6 +20,8 @@ namespace CryptoExchange.Net.UnitTests
[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)
@@ -162,6 +164,44 @@ namespace CryptoExchange.Net.UnitTests
var output = JsonConvert.DeserializeObject<NotNullableEnumObject>($"{{ \"Value\": {val} }}");
Assert.AreEqual(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 = JsonConvert.DeserializeObject<BoolObject>($"{{ \"Value\": {val} }}");
Assert.AreEqual(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 = JsonConvert.DeserializeObject<NotNullableBoolObject>($"{{ \"Value\": {val} }}");
Assert.AreEqual(output.Value, expected);
}
}
public class TimeObject
@@ -180,6 +220,18 @@ namespace CryptoExchange.Net.UnitTests
public TestEnum Value { get; set; }
}
public class BoolObject
{
[JsonConverter(typeof(BoolConverter))]
public bool? Value { get; set; }
}
public class NotNullableBoolObject
{
[JsonConverter(typeof(BoolConverter))]
public bool Value { get; set; }
}
[JsonConverter(typeof(EnumConverter))]
public enum TestEnum
{
@@ -7,7 +7,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0-preview-20211130-02"></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="NUnit3TestAdapter" Version="4.2.0"></PackageReference>
</ItemGroup>
+48 -190
View File
@@ -1,5 +1,6 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.UnitTests.TestImplementations;
using Microsoft.Extensions.Logging;
using NUnit.Framework;
@@ -34,7 +35,7 @@ namespace CryptoExchange.Net.UnitTests
// act
// assert
Assert.Throws(typeof(ArgumentException),
() => new RestApiClientOptions() { ApiCredentials = new ApiCredentials(key, secret) });
() => new RestExchangeOptions<TestEnvironment, ApiCredentials>() { ApiCredentials = new ApiCredentials(key, secret) });
}
[Test]
@@ -57,239 +58,96 @@ namespace CryptoExchange.Net.UnitTests
public void TestApiOptionsAreSet()
{
// arrange, act
var options = new TestClientOptions
{
Api1Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("123", "456"),
BaseAddress = "http://test1.com"
},
Api2Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("789", "101"),
BaseAddress = "http://test2.com"
}
};
var options = new TestClientOptions();
options.Api1Options.ApiCredentials = new ApiCredentials("123", "456");
options.Api2Options.ApiCredentials = new ApiCredentials("789", "101");
// assert
Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "123");
Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "456");
Assert.AreEqual(options.Api1Options.BaseAddress, "http://test1.com");
Assert.AreEqual(options.Api2Options.ApiCredentials.Key.GetString(), "789");
Assert.AreEqual(options.Api2Options.ApiCredentials.Secret.GetString(), "101");
Assert.AreEqual(options.Api2Options.BaseAddress, "http://test2.com");
}
[Test]
public void TestNotOverridenApiOptionsAreStillDefault()
{
// arrange, act
var options = new TestClientOptions
{
Api1Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("123", "456"),
}
};
// assert
Assert.AreEqual(options.Api1Options.RateLimitingBehaviour, RateLimitingBehaviour.Wait);
Assert.AreEqual(options.Api1Options.BaseAddress, "https://api1.test.com/");
Assert.AreEqual(options.Api2Options.BaseAddress, "https://api2.test.com/");
}
[Test]
public void TestSettingDefaultBaseOptionsAreRespected()
{
// arrange
TestClientOptions.Default = new TestClientOptions
{
ApiCredentials = new ApiCredentials("123", "456"),
LogLevel = LogLevel.Trace
};
// act
var options = new TestClientOptions();
// assert
Assert.AreEqual(options.LogLevel, LogLevel.Trace);
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
}
[Test]
public void TestSettingDefaultApiOptionsAreRespected()
{
// arrange
TestClientOptions.Default = new TestClientOptions
{
ApiCredentials = new ApiCredentials("123", "456"),
LogLevel = LogLevel.Trace,
Api1Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("456", "789")
}
};
// act
var options = new TestClientOptions();
// assert
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
Assert.AreEqual(options.Api1Options.BaseAddress, "https://api1.test.com/");
Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "456");
Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "789");
}
[Test]
public void TestSettingDefaultApiOptionsWithSomeOverriddenAreRespected()
{
// arrange
TestClientOptions.Default = new TestClientOptions
{
ApiCredentials = new ApiCredentials("123", "456"),
LogLevel = LogLevel.Trace,
Api1Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("456", "789")
},
Api2Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("111", "222")
}
};
// act
var options = new TestClientOptions
{
Api1Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("333", "444")
}
};
// assert
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "333");
Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "444");
Assert.AreEqual(options.Api2Options.ApiCredentials.Key.GetString(), "111");
Assert.AreEqual(options.Api2Options.ApiCredentials.Secret.GetString(), "222");
}
[Test]
public void TestClientUsesCorrectOptions()
{
var client = new TestRestClient(new TestClientOptions()
{
ApiCredentials = new ApiCredentials("123", "456"),
Api1Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("111", "222")
}
var client = new TestRestClient(options => {
options.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
options.ApiCredentials = new ApiCredentials("333", "444");
});
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Key.GetString(), "111");
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Secret.GetString(), "222");
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Key.GetString(), "123");
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Secret.GetString(), "456");
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
Assert.AreEqual(authProvider1.GetKey(), "111");
Assert.AreEqual(authProvider1.GetSecret(), "222");
Assert.AreEqual(authProvider2.GetKey(), "333");
Assert.AreEqual(authProvider2.GetSecret(), "444");
}
[Test]
public void TestClientUsesCorrectOptionsWithDefault()
{
TestClientOptions.Default = new TestClientOptions()
{
ApiCredentials = new ApiCredentials("123", "456"),
Api1Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("111", "222")
}
};
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
var client = new TestRestClient();
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Key.GetString(), "111");
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Secret.GetString(), "222");
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Key.GetString(), "123");
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Secret.GetString(), "456");
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
Assert.AreEqual(authProvider1.GetKey(), "111");
Assert.AreEqual(authProvider1.GetSecret(), "222");
Assert.AreEqual(authProvider2.GetKey(), "123");
Assert.AreEqual(authProvider2.GetSecret(), "456");
}
[Test]
public void TestClientUsesCorrectOptionsWithOverridingDefault()
{
TestClientOptions.Default = new TestClientOptions()
{
ApiCredentials = new ApiCredentials("123", "456"),
Api1Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("111", "222")
}
};
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
var client = new TestRestClient(new TestClientOptions
var client = new TestRestClient(options =>
{
Api1Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("333", "444")
},
Api2Options = new RestApiClientOptions()
{
BaseAddress = "http://test.com"
}
options.Api1Options.ApiCredentials = new ApiCredentials("333", "444");
options.Environment = new TestEnvironment("Test", "https://test.test");
});
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Key.GetString(), "333");
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Secret.GetString(), "444");
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Key.GetString(), "123");
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Secret.GetString(), "456");
Assert.AreEqual(client.Api2.BaseAddress, "http://test.com");
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
Assert.AreEqual(authProvider1.GetKey(), "333");
Assert.AreEqual(authProvider1.GetSecret(), "444");
Assert.AreEqual(authProvider2.GetKey(), "123");
Assert.AreEqual(authProvider2.GetSecret(), "456");
Assert.AreEqual(client.Api2.BaseAddress, "https://localhost:123");
}
}
public class TestClientOptions: ClientOptions
public class TestClientOptions: RestExchangeOptions<TestEnvironment, ApiCredentials>
{
/// <summary>
/// Default options for the futures client
/// </summary>
public static TestClientOptions Default { get; set; } = new TestClientOptions();
public static TestClientOptions Default { get; set; } = new TestClientOptions()
{
Environment = new TestEnvironment("test", "https://test.com")
};
/// <summary>
/// The default receive window for requests
/// </summary>
public TimeSpan ReceiveWindow { get; set; } = TimeSpan.FromSeconds(5);
private RestApiClientOptions _api1Options = new RestApiClientOptions("https://api1.test.com/");
public RestApiClientOptions Api1Options
public RestApiOptions Api1Options { get; private set; } = new RestApiOptions();
public RestApiOptions Api2Options { get; set; } = new RestApiOptions();
internal TestClientOptions Copy()
{
get => _api1Options;
set => _api1Options = new RestApiClientOptions(_api1Options, value);
}
private RestApiClientOptions _api2Options = new RestApiClientOptions("https://api2.test.com/");
public RestApiClientOptions Api2Options
{
get => _api2Options;
set => _api2Options = new RestApiClientOptions(_api2Options, value);
}
/// <summary>
/// ctor
/// </summary>
public TestClientOptions(): this(Default)
{
}
public TestClientOptions(TestClientOptions baseOn): base(baseOn)
{
if (baseOn == null)
return;
ReceiveWindow = baseOn.ReceiveWindow;
Api1Options = new RestApiClientOptions(baseOn.Api1Options, null);
Api2Options = new RestApiClientOptions(baseOn.Api2Options, null);
var options = Copy<TestClientOptions>();
options.Api1Options = Api1Options.Copy<RestApiOptions>();
options.Api2Options = Api2Options.Copy<RestApiOptions>();
return options;
}
}
}
+22 -63
View File
@@ -11,7 +11,6 @@ using CryptoExchange.Net.Interfaces;
using Microsoft.Extensions.Logging;
using System.Net.Http;
using System.Threading.Tasks;
using CryptoExchange.Net.Logging;
using System.Threading;
namespace CryptoExchange.Net.UnitTests
@@ -106,23 +105,16 @@ namespace CryptoExchange.Net.UnitTests
{
// arrange
// act
var client = new TestRestClient(new TestClientOptions()
{
Api1Options = new RestApiClientOptions
{
BaseAddress = "http://test.address.com",
RateLimiters = new List<IRateLimiter> { new RateLimiter() },
RateLimitingBehaviour = RateLimitingBehaviour.Fail,
RequestTimeout = TimeSpan.FromMinutes(1)
}
});
var options = new TestClientOptions();
options.Api1Options.RateLimiters = new List<IRateLimiter> { new RateLimiter() };
options.Api1Options.RateLimitingBehaviour = RateLimitingBehaviour.Fail;
options.RequestTimeout = TimeSpan.FromMinutes(1);
var client = new TestBaseClient(options);
// assert
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.BaseAddress == "http://test.address.com");
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimiters.Count == 1);
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimitingBehaviour == RateLimitingBehaviour.Fail);
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RequestTimeout == TimeSpan.FromMinutes(1));
Assert.IsTrue(((TestClientOptions)client.ClientOptions).RequestTimeout == TimeSpan.FromMinutes(1));
}
[TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
@@ -136,13 +128,7 @@ namespace CryptoExchange.Net.UnitTests
{
// arrange
// act
var client = new TestRestClient(new TestClientOptions()
{
Api1Options = new RestApiClientOptions
{
BaseAddress = "http://test.address.com"
}
});
var client = new TestRestClient();
client.Api1.SetParameterPosition(new HttpMethod(method), pos);
@@ -175,20 +161,17 @@ namespace CryptoExchange.Net.UnitTests
[TestCase(1, 2)]
public async Task PartialEndpointRateLimiterBasics(int requests, double perSeconds)
{
var log = new Log("Test");
log.Level = LogLevel.Trace;
var rateLimiter = new RateLimiter();
rateLimiter.AddPartialEndpointLimit("/sapi/", requests, TimeSpan.FromSeconds(perSeconds));
for (var i = 0; i < requests + 1; i++)
{
var result1 = await rateLimiter.LimitRequestAsync(log, "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.IsTrue(i == requests? result1.Data > 1 : result1.Data == 0);
}
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
var result2 = await rateLimiter.LimitRequestAsync(log, "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.IsTrue(result2.Data == 0);
}
@@ -199,15 +182,12 @@ namespace CryptoExchange.Net.UnitTests
[TestCase("/sapi/", true)]
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
{
var log = new Log("Test");
log.Level = LogLevel.Trace;
var rateLimiter = new RateLimiter();
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1));
for (var i = 0; i < 2; i++)
{
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
bool expected = i == 1 ? (expectLimiting ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
Assert.IsTrue(expected);
}
@@ -218,14 +198,11 @@ namespace CryptoExchange.Net.UnitTests
[TestCase("/sapi/test", "/sapi/", false)]
public async Task PartialEndpointRateLimiterEndpoints(string endpoint1, string endpoint2, bool expectLimiting)
{
var log = new Log("Test");
log.Level = LogLevel.Trace;
var rateLimiter = new RateLimiter();
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1), countPerEndpoint: true);
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint2, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.IsTrue(result1.Data == 0);
Assert.IsTrue(expectLimiting ? result2.Data > 0 : result2.Data == 0);
}
@@ -236,20 +213,17 @@ namespace CryptoExchange.Net.UnitTests
[TestCase(1, 2)]
public async Task EndpointRateLimiterBasics(int requests, double perSeconds)
{
var log = new Log("Test");
log.Level = LogLevel.Trace;
var rateLimiter = new RateLimiter();
rateLimiter.AddEndpointLimit("/sapi/test", requests, TimeSpan.FromSeconds(perSeconds));
for (var i = 0; i < requests + 1; i++)
{
var result1 = await rateLimiter.LimitRequestAsync(log, "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.IsTrue(i == requests ? result1.Data > 1 : result1.Data == 0);
}
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
var result2 = await rateLimiter.LimitRequestAsync(log, "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.IsTrue(result2.Data == 0);
}
@@ -258,15 +232,12 @@ namespace CryptoExchange.Net.UnitTests
[TestCase("/sapi/test/123", false)]
public async Task EndpointRateLimiterEndpoints(string endpoint, bool expectLimited)
{
var log = new Log("Test");
log.Level = LogLevel.Trace;
var rateLimiter = new RateLimiter();
rateLimiter.AddEndpointLimit("/sapi/test", 1, TimeSpan.FromSeconds(0.1));
for (var i = 0; i < 2; i++)
{
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
Assert.IsTrue(expected);
}
@@ -278,15 +249,12 @@ namespace CryptoExchange.Net.UnitTests
[TestCase("/sapi/test23", false)]
public async Task EndpointRateLimiterMultipleEndpoints(string endpoint, bool expectLimited)
{
var log = new Log("Test");
log.Level = LogLevel.Trace;
var rateLimiter = new RateLimiter();
rateLimiter.AddEndpointLimit(new[] { "/sapi/test", "/sapi/test2" }, 1, TimeSpan.FromSeconds(0.1));
for (var i = 0; i < 2; i++)
{
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
Assert.IsTrue(expected);
}
@@ -315,14 +283,11 @@ namespace CryptoExchange.Net.UnitTests
[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 log = new Log("Test");
log.Level = LogLevel.Trace;
var rateLimiter = new RateLimiter();
rateLimiter.AddApiKeyLimit(1, TimeSpan.FromSeconds(0.1), onlyForSignedRequests, false);
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint1, HttpMethod.Get, signed1, key1?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint2, HttpMethod.Get, signed2, key2?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, signed1, key1?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, signed2, key2?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.IsTrue(result1.Data == 0);
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
}
@@ -332,14 +297,11 @@ namespace CryptoExchange.Net.UnitTests
[TestCase("/", "/sapi/test2", true)]
public async Task TotalRateLimiterBasics(string endpoint1, string endpoint2, bool expectLimited)
{
var log = new Log("Test");
log.Level = LogLevel.Trace;
var rateLimiter = new RateLimiter();
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint2, HttpMethod.Get, true, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, true, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.IsTrue(result1.Data == 0);
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
}
@@ -350,15 +312,12 @@ namespace CryptoExchange.Net.UnitTests
[TestCase("/sapi/test", true, true, false, true)]
public async Task ApiKeyRateLimiterIgnores_TotalRateLimiter_IfSet(string endpoint, bool signed1, bool signed2, bool ignoreTotal, bool expectLimited)
{
var log = new Log("Test");
log.Level = LogLevel.Trace;
var rateLimiter = new RateLimiter();
rateLimiter.AddApiKeyLimit(100, TimeSpan.FromSeconds(0.1), true, ignoreTotal);
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, signed1, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, signed2, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, signed1, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, signed2, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.IsTrue(result1.Data == 0);
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
}
@@ -1,12 +1,17 @@
using System;
using System.Collections.Generic;
using System.Threading;
using CryptoExchange.Net.Logging;
using System.Threading.Tasks;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.UnitTests.TestImplementations;
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
using Microsoft.Extensions.Logging;
using Moq;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using NUnit.Framework.Constraints;
namespace CryptoExchange.Net.UnitTests
{
@@ -18,19 +23,16 @@ namespace CryptoExchange.Net.UnitTests
{
//arrange
//act
var client = new TestSocketClient(new TestOptions()
var client = new TestSocketClient(options =>
{
SubOptions = new SocketApiClientOptions
{
BaseAddress = "http://test.address.com",
ReconnectInterval = TimeSpan.FromSeconds(6)
}
options.SubOptions.ApiCredentials = new Authentication.ApiCredentials("1", "2");
options.SubOptions.MaxSocketConnections = 1;
});
//assert
Assert.IsTrue(client.SubClient.Options.BaseAddress == "http://test.address.com");
Assert.IsTrue(client.SubClient.Options.ReconnectInterval.TotalSeconds == 6);
Assert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
Assert.AreEqual(1, client.SubClient.ApiOptions.MaxSocketConnections);
}
[TestCase(true)]
@@ -43,74 +45,70 @@ namespace CryptoExchange.Net.UnitTests
socket.CanConnect = canConnect;
//act
var connectResult = client.SubClient.ConnectSocketSub(new SocketConnection(new Log(""), client.SubClient, socket, null));
var connectResult = client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, null));
//assert
Assert.IsTrue(connectResult.Success == canConnect);
}
[TestCase]
public void SocketMessages_Should_BeProcessedInDataHandlers()
public async Task SocketMessages_Should_BeProcessedInDataHandlers()
{
// arrange
var client = new TestSocketClient(new TestOptions() {
SubOptions = new SocketApiClientOptions
{
ReconnectInterval = TimeSpan.Zero,
},
LogLevel = LogLevel.Debug
var client = new TestSocketClient(options => {
options.ReconnectInterval = TimeSpan.Zero;
});
var socket = client.CreateSocket();
socket.ShouldReconnect = true;
socket.CanConnect = true;
socket.DisconnectTime = DateTime.UtcNow;
var sub = new SocketConnection(new Log(""), client.SubClient, socket, null);
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
var rstEvent = new ManualResetEvent(false);
JToken result = null;
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, false, (messageEvent) =>
{
result = messageEvent.JsonData;
rstEvent.Set();
}));
Dictionary<string, string> result = null;
client.SubClient.ConnectSocketSub(sub);
sub.AddSubscription(new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
{
result = messageEvent.Data;
rstEvent.Set();
}));
// act
socket.InvokeMessage("{\"property\": 123}");
await socket.InvokeMessage("{\"property\": \"123\", \"topic\": \"topic\"}");
rstEvent.WaitOne(1000);
// assert
Assert.IsTrue((int)result["property"] == 123);
Assert.IsTrue(result["property"] == "123");
}
[TestCase(false)]
[TestCase(true)]
public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
public async Task SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
{
// arrange
var client = new TestSocketClient(new TestOptions() {
SubOptions = new SocketApiClientOptions
{
ReconnectInterval = TimeSpan.Zero,
OutputOriginalData = enabled
},
LogLevel = LogLevel.Debug,
var client = new TestSocketClient(options =>
{
options.ReconnectInterval = TimeSpan.Zero;
options.SubOptions.OutputOriginalData = enabled;
});
var socket = client.CreateSocket();
socket.ShouldReconnect = true;
socket.CanConnect = true;
socket.DisconnectTime = DateTime.UtcNow;
var sub = new SocketConnection(new Log(""), client.SubClient, socket, null);
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
var rstEvent = new ManualResetEvent(false);
string original = null;
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, false, (messageEvent) =>
client.SubClient.ConnectSocketSub(sub);
sub.AddSubscription(new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
{
original = messageEvent.OriginalData;
rstEvent.Set();
}));
client.SubClient.ConnectSocketSub(sub);
// act
socket.InvokeMessage("{\"property\": 123}");
await socket.InvokeMessage("{\"property\": 123}");
rstEvent.WaitOne(1000);
// assert
@@ -121,21 +119,18 @@ namespace CryptoExchange.Net.UnitTests
public void UnsubscribingStream_Should_CloseTheSocket()
{
// arrange
var client = new TestSocketClient(new TestOptions()
var client = new TestSocketClient(options =>
{
SubOptions = new SocketApiClientOptions
{
ReconnectInterval = TimeSpan.Zero,
},
LogLevel = LogLevel.Debug
});
options.ReconnectInterval = TimeSpan.Zero;
});
var socket = client.CreateSocket();
socket.CanConnect = true;
var sub = new SocketConnection(new Log(""), client.SubClient, socket, null);
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
client.SubClient.ConnectSocketSub(sub);
var us = SocketSubscription.CreateForIdentifier(10, "Test", true, false, (e) => { });
var ups = new UpdateSubscription(sub, us);
sub.AddSubscription(us);
var subscription = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
var ups = new UpdateSubscription(sub, subscription);
sub.AddSubscription(subscription);
// act
client.UnsubscribeAsync(ups).Wait();
@@ -148,22 +143,22 @@ namespace CryptoExchange.Net.UnitTests
public void UnsubscribingAll_Should_CloseAllSockets()
{
// arrange
var client = new TestSocketClient(new TestOptions()
{
SubOptions = new SocketApiClientOptions
{
ReconnectInterval = TimeSpan.Zero,
},
LogLevel = LogLevel.Debug
});
var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
var socket1 = client.CreateSocket();
var socket2 = client.CreateSocket();
socket1.CanConnect = true;
socket2.CanConnect = true;
var sub1 = new SocketConnection(new Log(""), client.SubClient, socket1, null);
var sub2 = new SocketConnection(new Log(""), client.SubClient, socket2, null);
var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket1, null);
var sub2 = new SocketConnection(new TraceLogger(), client.SubClient, socket2, null);
client.SubClient.ConnectSocketSub(sub1);
client.SubClient.ConnectSocketSub(sub2);
var subscription1 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
var subscription2 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
sub1.AddSubscription(subscription1);
sub2.AddSubscription(subscription2);
var ups1 = new UpdateSubscription(sub1, subscription1);
var ups2 = new UpdateSubscription(sub2, subscription2);
// act
client.UnsubscribeAllAsync().Wait();
@@ -177,17 +172,10 @@ namespace CryptoExchange.Net.UnitTests
public void FailingToConnectSocket_Should_ReturnError()
{
// arrange
var client = new TestSocketClient(new TestOptions()
{
SubOptions = new SocketApiClientOptions
{
ReconnectInterval = TimeSpan.Zero,
},
LogLevel = LogLevel.Debug
});
var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
var socket = client.CreateSocket();
socket.CanConnect = false;
var sub1 = new SocketConnection(new Log(""), client.SubClient, socket, null);
var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
// act
var connectResult = client.SubClient.ConnectSocketSub(sub1);
@@ -4,8 +4,9 @@ using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.OrderBook;
using CryptoExchange.Net.Sockets;
using NUnit.Framework;
namespace CryptoExchange.Net.UnitTests
@@ -13,12 +14,13 @@ namespace CryptoExchange.Net.UnitTests
[TestFixture]
public class SymbolOrderBookTests
{
private static OrderBookOptions defaultOrderBookOptions = new OrderBookOptions();
private static readonly OrderBookOptions _defaultOrderBookOptions = new OrderBookOptions();
private class TestableSymbolOrderBook : SymbolOrderBook
{
public TestableSymbolOrderBook() : base("Test", "BTC/USD", defaultOrderBookOptions)
public TestableSymbolOrderBook() : base(null, "Test", "BTC/USD")
{
Initialize(_defaultOrderBookOptions);
}
@@ -35,12 +37,12 @@ namespace CryptoExchange.Net.UnitTests
public void SetData(IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
{
Status = OrderBookStatus.Synced;
base.bids.Clear();
base._bids.Clear();
foreach (var bid in bids)
base.bids.Add(bid.Price, bid);
base.asks.Clear();
base._bids.Add(bid.Price, bid);
base._asks.Clear();
foreach (var ask in asks)
base.asks.Add(ask.Price, ask);
base._asks.Add(ask.Price, ask);
}
}
@@ -0,0 +1,19 @@
using CryptoExchange.Net.Sockets;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{
internal class TestQuery : Query<object>
{
public override HashSet<string> ListenerIdentifiers { get; set; }
public TestQuery(string identifier, object request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
{
ListenerIdentifiers = new HashSet<string> { identifier };
}
}
}
@@ -0,0 +1,36 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{
internal class TestSubscription<T> : Subscription<object, object>
{
private readonly Action<DataEvent<T>> _handler;
public override HashSet<string> ListenerIdentifiers { get; set; } = new HashSet<string> { "topic" };
public TestSubscription(ILogger logger, Action<DataEvent<T>> handler) : base(logger, false)
{
_handler = handler;
}
public override Task<CallResult> DoHandleMessageAsync(SocketConnection connection, DataEvent<object> message)
{
var data = (T)message.Data;
_handler.Invoke(message.As(data));
return Task.FromResult(new CallResult(null));
}
public override Type GetMessageType(IMessageAccessor message) => typeof(T);
public override Query GetSubQuery(SocketConnection connection) => new TestQuery("sub", new object(), false, 1);
public override Query GetUnsubQuery() => new TestQuery("unsub", new object(), false, 1);
}
}
@@ -3,8 +3,8 @@ using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.UnitTests.TestImplementations;
using Microsoft.Extensions.Logging;
@@ -14,24 +14,28 @@ namespace CryptoExchange.Net.UnitTests
{
public TestSubClient SubClient { get; }
public TestBaseClient(): base("Test", new TestOptions())
public TestBaseClient(): base(null, "Test")
{
SubClient = AddApiClient(new TestSubClient(new TestOptions(), new RestApiClientOptions()));
var options = TestClientOptions.Default.Copy();
Initialize(options);
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
}
public TestBaseClient(ClientOptions exchangeOptions) : base("Test", exchangeOptions)
public TestBaseClient(TestClientOptions exchangeOptions) : base(null, "Test")
{
Initialize(exchangeOptions);
SubClient = AddApiClient(new TestSubClient(exchangeOptions, new RestApiOptions()));
}
public void Log(LogLevel verbosity, string data)
{
log.Write(verbosity, data);
_logger.Log(verbosity, data);
}
}
public class TestSubClient : RestApiClient
{
public TestSubClient(ClientOptions options, RestApiClientOptions apiOptions) : base(new Log(""), options, apiOptions)
public TestSubClient(RestExchangeOptions<TestEnvironment> options, RestApiOptions apiOptions) : base(new TraceLogger(), null, "https://localhost:123", options, apiOptions)
{
}
@@ -56,9 +60,7 @@ namespace CryptoExchange.Net.UnitTests
headers = new Dictionary<string, string>();
}
public override string Sign(string toSign)
{
return toSign;
}
public string GetKey() => _credentials.Key.GetString();
public string GetSecret() => _credentials.Secret.GetString();
}
}
@@ -12,7 +12,8 @@ using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using System.Collections.Generic;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
@@ -21,14 +22,22 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public TestRestApi1Client Api1 { get; }
public TestRestApi2Client Api2 { get; }
public TestRestClient() : this(new TestClientOptions())
public TestRestClient(Action<TestClientOptions> optionsFunc) : this(optionsFunc, null)
{
}
public TestRestClient(TestClientOptions exchangeOptions) : base("Test", exchangeOptions)
public TestRestClient(ILoggerFactory loggerFactory = null, HttpClient httpClient = null) : this((x) => { }, httpClient, loggerFactory)
{
Api1 = new TestRestApi1Client(exchangeOptions);
Api2 = new TestRestApi2Client(exchangeOptions);
}
public TestRestClient(Action<TestClientOptions> optionsFunc, HttpClient httpClient = null, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
{
var options = TestClientOptions.Default.Copy();
optionsFunc(options);
Initialize(options);
Api1 = new TestRestApi1Client(options);
Api2 = new TestRestApi2Client(options);
}
public void SetResponse(string responseData, out IRequest requestObj)
@@ -122,7 +131,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public class TestRestApi1Client : RestApiClient
{
public TestRestApi1Client(TestClientOptions options): base(new Log(""), options, options.Api1Options)
public TestRestApi1Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api1Options)
{
RequestFactory = new Mock<IRequestFactory>().Object;
}
@@ -163,7 +172,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public class TestRestApi2Client : RestApiClient
{
public TestRestApi2Client(TestClientOptions options) : base(new Log(""), options, options.Api2Options)
public TestRestApi2Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api2Options)
{
RequestFactory = new Mock<IRequestFactory>().Object;
}
@@ -173,9 +182,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
}
protected override Error ParseErrorResponse(JToken error)
protected override Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, string data)
{
return new ServerError((int)error["errorCode"], (string)error["errorMessage"]);
var errorData = ValidateJson(data);
return new ServerError((int)errorData.Data["errorCode"], (string)errorData.Data["errorMessage"]);
}
public override TimeSpan? GetTimeOffset()
@@ -197,24 +208,9 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
}
}
public class TestAuthProvider : AuthenticationProvider
{
public TestAuthProvider(ApiCredentials credentials) : base(credentials)
{
}
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)
{
uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(providedParameters) : new SortedDictionary<string, object>();
bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(providedParameters) : new SortedDictionary<string, object>();
headers = new Dictionary<string, string>();
}
}
public class ParseErrorTestRestClient: TestRestClient
{
public ParseErrorTestRestClient() { }
public ParseErrorTestRestClient(TestClientOptions exchangeOptions) : base(exchangeOptions) { }
}
}
@@ -1,4 +1,6 @@
using System;
using System.IO;
using System.Net.WebSockets;
using System.Security.Authentication;
using System.Text;
using System.Threading.Tasks;
@@ -12,15 +14,15 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public bool CanConnect { get; set; }
public bool Connected { get; set; }
public event Action OnClose;
public event Func<Task> OnClose;
#pragma warning disable 0067
public event Action OnReconnected;
public event Action OnReconnecting;
public event Func<Task> OnReconnected;
public event Func<Task> OnReconnecting;
#pragma warning restore 0067
public event Action<string> OnMessage;
public event Action<Exception> OnError;
public event Action OnOpen;
public event Func<int, Task> OnRequestSent;
public event Func<WebSocketMessageType, Stream, Task> OnStreamMessage;
public event Func<Exception, Task> OnError;
public event Func<Task> OnOpen;
public Func<Task<Uri>> GetReconnectionUrl { get; set; }
public int Id { get; }
@@ -69,10 +71,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
return Task.FromResult(CanConnect);
}
public void Send(string data)
public void Send(int requestId, string data, int weight)
{
if(!Connected)
throw new Exception("Socket not connected");
OnRequestSent?.Invoke(requestId);
}
public void Reset()
@@ -108,9 +111,10 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
OnOpen?.Invoke();
}
public void InvokeMessage(string data)
public async Task InvokeMessage(string data)
{
OnMessage?.Invoke(data);
var stream = new MemoryStream(Encoding.UTF8.GetBytes(data));
await OnStreamMessage?.Invoke(WebSocketMessageType.Text, stream);
}
public void SetProxy(ApiProxy proxy)
@@ -1,10 +1,14 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
using Microsoft.Extensions.Logging;
using Moq;
using Newtonsoft.Json.Linq;
@@ -14,34 +18,61 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
{
public TestSubSocketClient SubClient { get; }
public TestSocketClient() : this(new TestOptions())
public TestSocketClient(ILoggerFactory loggerFactory = null) : this((x) => { }, loggerFactory)
{
}
public TestSocketClient(TestOptions exchangeOptions) : base("test", exchangeOptions)
/// <summary>
/// Create a new instance of KucoinSocketClient
/// </summary>
/// <param name="optionsFunc">Configure the options to use for this client</param>
public TestSocketClient(Action<TestSocketOptions> optionsFunc) : this(optionsFunc, null)
{
SubClient = AddApiClient(new TestSubSocketClient(exchangeOptions, exchangeOptions.SubOptions));
}
public TestSocketClient(Action<TestSocketOptions> optionsFunc, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
{
var options = TestSocketOptions.Default.Copy<TestSocketOptions>();
optionsFunc(options);
Initialize(options);
SubClient = AddApiClient(new TestSubSocketClient(options, options.SubOptions));
SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object;
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
}
public TestSocket CreateSocket()
{
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
return (TestSocket)SubClient.CreateSocketInternal("https://localhost:123/");
}
}
public class TestOptions: ClientOptions
public class TestEnvironment : TradeEnvironment
{
public SocketApiClientOptions SubOptions { get; set; } = new SocketApiClientOptions();
public string TestAddress { get; }
public TestEnvironment(string name, string url) : base(name)
{
TestAddress = url;
}
}
public class TestSocketOptions: SocketExchangeOptions<TestEnvironment>
{
public static TestSocketOptions Default = new TestSocketOptions
{
Environment = new TestEnvironment("Live", "https://test.test")
};
public SocketApiOptions SubOptions { get; set; } = new SocketApiOptions();
}
public class TestSubSocketClient : SocketApiClient
{
public TestSubSocketClient(ClientOptions options, SocketApiClientOptions apiOptions): base(new Log(""), options, apiOptions)
public TestSubSocketClient(TestSocketOptions options, SocketApiOptions apiOptions): base(new TraceLogger(), options.Environment.TestAddress, options, apiOptions)
{
}
@@ -59,35 +90,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
return ConnectSocketAsync(sub).Result;
}
protected internal override bool HandleQueryResponse<T>(SocketConnection s, object request, JToken data, out CallResult<T> callResult)
{
throw new NotImplementedException();
}
protected internal override bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message,
out CallResult<object> callResult)
{
throw new NotImplementedException();
}
protected internal override bool MessageMatchesHandler(SocketConnection s, JToken message, object request)
{
throw new NotImplementedException();
}
protected internal override bool MessageMatchesHandler(SocketConnection s, JToken message, string identifier)
{
return true;
}
protected internal override Task<CallResult<bool>> AuthenticateSocketAsync(SocketConnection s)
{
throw new NotImplementedException();
}
protected internal override Task<bool> UnsubscribeAsync(SocketConnection connection, SocketSubscription s)
{
throw new NotImplementedException();
}
public override string GetListenerIdentifier(IMessageAccessor messageAccessor) => "topic";
}
}
@@ -1,25 +0,0 @@
using Microsoft.Extensions.Logging;
using System;
using System.Text;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
public class TestStringLogger : ILogger
{
StringBuilder _builder = new StringBuilder();
public IDisposable BeginScope<TState>(TState state) => null;
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
_builder.AppendLine(formatter(state, exception));
}
public string GetLogs()
{
return _builder.ToString();
}
}
}
+6 -1
View File
@@ -1 +1,6 @@
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")]
namespace System.Runtime.CompilerServices
{
internal static class IsExternalInit { }
}
@@ -21,16 +21,32 @@ namespace CryptoExchange.Net.Authentication
/// </summary>
public SecureString? Secret { get; }
/// <summary>
/// Type of the credentials
/// </summary>
public ApiCredentialsType CredentialType { get; }
/// <summary>
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
/// <param name="key">The api key used for identification</param>
/// <param name="secret">The api secret used for signing</param>
public ApiCredentials(SecureString key, SecureString secret)
public ApiCredentials(SecureString key, SecureString secret) : this(key, secret, ApiCredentialsType.Hmac)
{
}
/// <summary>
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
/// <param name="key">The api key used for identification</param>
/// <param name="secret">The api secret used for signing</param>
/// <param name="credentialsType">The type of credentials</param>
public ApiCredentials(SecureString key, SecureString secret, ApiCredentialsType credentialsType)
{
if (key == null || secret == null)
throw new ArgumentException("Key and secret can't be null/empty");
CredentialType = credentialsType;
Key = key;
Secret = secret;
}
@@ -40,11 +56,22 @@ namespace CryptoExchange.Net.Authentication
/// </summary>
/// <param name="key">The api key used for identification</param>
/// <param name="secret">The api secret used for signing</param>
public ApiCredentials(string key, string secret)
public ApiCredentials(string key, string secret) : this(key, secret, ApiCredentialsType.Hmac)
{
}
/// <summary>
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
/// <param name="key">The api key used for identification</param>
/// <param name="secret">The api secret used for signing</param>
/// <param name="credentialsType">The type of credentials</param>
public ApiCredentials(string key, string secret, ApiCredentialsType credentialsType)
{
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
throw new ArgumentException("Key and secret can't be null/empty");
CredentialType = credentialsType;
Key = key.ToSecureString();
Secret = secret.ToSecureString();
}
@@ -56,7 +83,7 @@ namespace CryptoExchange.Net.Authentication
public virtual ApiCredentials Copy()
{
// Use .GetString() to create a copy of the SecureString
return new ApiCredentials(Key!.GetString(), Secret!.GetString());
return new ApiCredentials(Key!.GetString(), Secret!.GetString(), CredentialType);
}
/// <summary>
@@ -0,0 +1,21 @@
namespace CryptoExchange.Net.Authentication
{
/// <summary>
/// Credentials type
/// </summary>
public enum ApiCredentialsType
{
/// <summary>
/// Hmac keys credentials
/// </summary>
Hmac,
/// <summary>
/// Rsa keys credentials in xml format
/// </summary>
RsaXml,
/// <summary>
/// Rsa keys credentials in pem/base64 format. Only available for .NetStandard 2.1 and up, use xml format for lower.
/// </summary>
RsaPem
}
}
@@ -12,14 +12,15 @@ namespace CryptoExchange.Net.Authentication
/// <summary>
/// Base class for authentication providers
/// </summary>
public abstract class AuthenticationProvider
public abstract class AuthenticationProvider : IDisposable
{
/// <summary>
/// The provided credentials
/// Provided credentials
/// </summary>
public ApiCredentials Credentials { get; }
protected readonly ApiCredentials _credentials;
/// <summary>
/// Byte representation of the secret
/// </summary>
protected byte[] _sBytes;
@@ -32,7 +33,7 @@ namespace CryptoExchange.Net.Authentication
if (credentials.Secret == null)
throw new ArgumentException("ApiKey/Secret needed");
Credentials = credentials;
_credentials = credentials;
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret.GetString());
}
@@ -73,6 +74,17 @@ namespace CryptoExchange.Net.Authentication
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
}
/// <summary>
/// SHA256 sign the data and return the bytes
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
protected static byte[] SignSHA256Bytes(byte[] data)
{
using var encryptor = SHA256.Create();
return encryptor.ComputeHash(data);
}
/// <summary>
/// SHA256 sign the data and return the hash
/// </summary>
@@ -83,7 +95,20 @@ namespace CryptoExchange.Net.Authentication
{
using var encryptor = SHA256.Create();
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes): BytesToHexString(resultBytes);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// SHA256 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected static string SignSHA256(byte[] data, SignOutputType? outputType = null)
{
using var encryptor = SHA256.Create();
var resultBytes = encryptor.ComputeHash(data);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
@@ -99,6 +124,41 @@ namespace CryptoExchange.Net.Authentication
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// SHA384 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected static string SignSHA384(byte[] data, SignOutputType? outputType = null)
{
using var encryptor = SHA384.Create();
var resultBytes = encryptor.ComputeHash(data);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// SHA384 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <returns></returns>
protected static byte[] SignSHA384Bytes(string data)
{
using var encryptor = SHA384.Create();
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
}
/// <summary>
/// SHA384 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <returns></returns>
protected static byte[] SignSHA384Bytes(byte[] data)
{
using var encryptor = SHA384.Create();
return encryptor.ComputeHash(data);
}
/// <summary>
/// SHA512 sign the data and return the hash
/// </summary>
@@ -112,6 +172,41 @@ namespace CryptoExchange.Net.Authentication
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// SHA512 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected static string SignSHA512(byte[] data, SignOutputType? outputType = null)
{
using var encryptor = SHA512.Create();
var resultBytes = encryptor.ComputeHash(data);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// SHA512 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <returns></returns>
protected static byte[] SignSHA512Bytes(string data)
{
using var encryptor = SHA512.Create();
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
}
/// <summary>
/// SHA512 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <returns></returns>
protected static byte[] SignSHA512Bytes(byte[] data)
{
using var encryptor = SHA512.Create();
return encryptor.ComputeHash(data);
}
/// <summary>
/// MD5 sign the data and return the hash
/// </summary>
@@ -126,28 +221,70 @@ namespace CryptoExchange.Net.Authentication
}
/// <summary>
/// HMACSHA256 sign the data and return the hash
/// MD5 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected static string SignMD5(byte[] data, SignOutputType? outputType = null)
{
using var encryptor = MD5.Create();
var resultBytes = encryptor.ComputeHash(data);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// MD5 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <returns></returns>
protected static byte[] SignMD5Bytes(string data)
{
using var encryptor = MD5.Create();
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
}
/// <summary>
/// HMACSHA512 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected string SignHMACSHA256(string data, SignOutputType? outputType = null)
=> SignHMACSHA256(Encoding.UTF8.GetBytes(data), outputType);
/// <summary>
/// HMACSHA256 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected string SignHMACSHA256(byte[] data, SignOutputType? outputType = null)
{
using var encryptor = new HMACSHA256(_sBytes);
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
var resultBytes = encryptor.ComputeHash(data);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// HMACSHA512 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected string SignHMACSHA384(string data, SignOutputType? outputType = null)
=> SignHMACSHA384(Encoding.UTF8.GetBytes(data), outputType);
/// <summary>
/// HMACSHA384 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected string SignHMACSHA384(string data, SignOutputType? outputType = null)
protected string SignHMACSHA384(byte[] data, SignOutputType? outputType = null)
{
using var encryptor = new HMACSHA384(_sBytes);
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
var resultBytes = encryptor.ComputeHash(data);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
@@ -174,23 +311,80 @@ namespace CryptoExchange.Net.Authentication
}
/// <summary>
/// Sign a string
/// SHA256 sign the data
/// </summary>
/// <param name="toSign"></param>
/// <param name="data"></param>
/// <param name="outputType"></param>
/// <returns></returns>
public virtual string Sign(string toSign)
protected string SignRSASHA256(byte[] data, SignOutputType? outputType = null)
{
return toSign;
using var rsa = CreateRSA();
using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(data);
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
return outputType == SignOutputType.Base64? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// Sign a byte array
/// SHA384 sign the data
/// </summary>
/// <param name="toSign"></param>
/// <param name="data"></param>
/// <param name="outputType"></param>
/// <returns></returns>
public virtual byte[] Sign(byte[] toSign)
protected string SignRSASHA384(byte[] data, SignOutputType? outputType = null)
{
return toSign;
using var rsa = CreateRSA();
using var sha384 = SHA384.Create();
var hash = sha384.ComputeHash(data);
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA384, RSASignaturePadding.Pkcs1);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// SHA512 sign the data
/// </summary>
/// <param name="data"></param>
/// <param name="outputType"></param>
/// <returns></returns>
protected string SignRSASHA512(byte[] data, SignOutputType? outputType = null)
{
using var rsa = CreateRSA();
using var sha512 = SHA512.Create();
var hash = sha512.ComputeHash(data);
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
private RSA CreateRSA()
{
var rsa = RSA.Create();
if (_credentials.CredentialType == ApiCredentialsType.RsaPem)
{
#if NETSTANDARD2_1_OR_GREATER
// Read from pem private key
var key = _credentials.Secret!.GetString()
.Replace("\n", "")
.Replace("-----BEGIN PRIVATE KEY-----", "")
.Replace("-----END PRIVATE KEY-----", "")
.Trim();
rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(
key)
, out _);
#else
throw new Exception("Pem format not supported when running from .NetStandard2.0. Convert the private key to xml format.");
#endif
}
else if (_credentials.CredentialType == ApiCredentialsType.RsaXml)
{
// Read from xml private key format
rsa.FromXmlString(_credentials.Secret!.GetString());
}
else
{
throw new Exception("Invalid credentials type");
}
return rsa;
}
/// <summary>
@@ -235,5 +429,26 @@ namespace CryptoExchange.Net.Authentication
{
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
}
/// <inheritdoc />
public void Dispose()
{
_credentials?.Dispose();
}
}
/// <inheritdoc />
public abstract class AuthenticationProvider<TApiCredentials> : AuthenticationProvider where TApiCredentials : ApiCredentials
{
/// <inheritdoc />
protected new TApiCredentials _credentials => (TApiCredentials)base._credentials;
/// <summary>
/// ctor
/// </summary>
/// <param name="credentials"></param>
protected AuthenticationProvider(TApiCredentials credentials) : base(credentials)
{
}
}
}
+53 -77
View File
@@ -1,14 +1,14 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@@ -20,14 +20,10 @@ namespace CryptoExchange.Net
/// </summary>
public abstract class BaseApiClient : IDisposable, IBaseApiClient
{
private ApiCredentials? _apiCredentials;
private AuthenticationProvider? _authenticationProvider;
private bool _created;
/// <summary>
/// Logger
/// </summary>
protected Log _log;
protected ILogger _logger;
/// <summary>
/// If we are disposing
@@ -37,19 +33,7 @@ namespace CryptoExchange.Net
/// <summary>
/// The authentication provider for this API client. (null if no credentials are set)
/// </summary>
public AuthenticationProvider? AuthenticationProvider
{
get
{
if (!_created && !_disposing && _apiCredentials != null)
{
_authenticationProvider = CreateAuthenticationProvider(_apiCredentials);
_created = true;
}
return _authenticationProvider;
}
}
public AuthenticationProvider? AuthenticationProvider { get; private set; }
/// <summary>
/// Where to put the parameters for requests with different Http methods
@@ -83,45 +67,53 @@ namespace CryptoExchange.Net
public string requestBodyEmptyContent = "{}";
/// <summary>
/// The base address for this API client
/// The environment this client communicates to
/// </summary>
internal protected string BaseAddress { get; }
public string BaseAddress { get; }
/// <summary>
/// Options
/// Output the original string data along with the deserialized object
/// </summary>
public ApiClientOptions Options { get; }
public bool OutputOriginalData { get; }
/// <summary>
/// The last used id, use NextId() to get the next id and up this
/// The default serializer
/// </summary>
protected static int lastId;
/// <summary>
/// Lock for id generating
/// </summary>
protected static object idLock = new();
protected virtual JsonSerializer DefaultSerializer { get; set; } = JsonSerializer.Create(SerializerOptions.Default);
/// <summary>
/// A default serializer
/// Api options
/// </summary>
private static readonly JsonSerializer _defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings
{
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
Culture = CultureInfo.InvariantCulture
});
public ApiOptions ApiOptions { get; }
/// <summary>
/// Client Options
/// </summary>
public ExchangeOptions ClientOptions { get; }
/// <summary>
/// ctor
/// </summary>
/// <param name="log">Logger</param>
/// <param name="logger">Logger</param>
/// <param name="outputOriginalData">Should data from this client include the orginal data in the call result</param>
/// <param name="baseAddress">Base address for this API client</param>
/// <param name="apiCredentials">Api credentials</param>
/// <param name="clientOptions">Client options</param>
/// <param name="apiOptions">Api client options</param>
protected BaseApiClient(Log log, ClientOptions clientOptions, ApiClientOptions apiOptions)
/// <param name="apiOptions">Api options</param>
protected BaseApiClient(ILogger logger, bool outputOriginalData, ApiCredentials? apiCredentials, string baseAddress, ExchangeOptions clientOptions, ApiOptions apiOptions)
{
Options = apiOptions;
_log = log;
_apiCredentials = apiOptions.ApiCredentials?.Copy() ?? clientOptions.ApiCredentials?.Copy();
BaseAddress = apiOptions.BaseAddress;
_logger = logger;
ClientOptions = clientOptions;
ApiOptions = apiOptions;
OutputOriginalData = outputOriginalData;
BaseAddress = baseAddress;
if (apiCredentials != null)
{
AuthenticationProvider?.Dispose();
AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy());
}
}
/// <summary>
@@ -134,9 +126,11 @@ namespace CryptoExchange.Net
/// <inheritdoc />
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
{
_apiCredentials = credentials?.Copy();
_created = false;
_authenticationProvider = null;
if (credentials != null)
{
AuthenticationProvider?.Dispose();
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
}
}
/// <summary>
@@ -149,7 +143,7 @@ namespace CryptoExchange.Net
if (string.IsNullOrEmpty(data))
{
var info = "Empty data object received";
_log.Write(LogLevel.Error, info);
_logger.Log(LogLevel.Error, info);
return new CallResult<JToken>(new DeserializeError(info, data));
}
@@ -188,7 +182,7 @@ namespace CryptoExchange.Net
var tokenResult = ValidateJson(data);
if (!tokenResult)
{
_log.Write(LogLevel.Error, tokenResult.Error!.Message);
_logger.Log(LogLevel.Error, tokenResult.Error!.Message);
return new CallResult<T>(tokenResult.Error);
}
@@ -205,7 +199,7 @@ namespace CryptoExchange.Net
/// <returns></returns>
protected CallResult<T> Deserialize<T>(JToken obj, JsonSerializer? serializer = null, int? requestId = null)
{
serializer ??= _defaultSerializer;
serializer ??= DefaultSerializer;
try
{
@@ -214,20 +208,20 @@ namespace CryptoExchange.Net
catch (JsonReaderException jre)
{
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message} Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {obj}";
_log.Write(LogLevel.Error, info);
_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}";
_log.Write(LogLevel.Error, info);
_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}";
_log.Write(LogLevel.Error, info);
_logger.Log(LogLevel.Error, info);
return new CallResult<T>(new DeserializeError(info, obj));
}
}
@@ -243,30 +237,26 @@ namespace CryptoExchange.Net
/// <returns></returns>
protected async Task<CallResult<T>> DeserializeAsync<T>(Stream stream, JsonSerializer? serializer = null, int? requestId = null, long? elapsedMilliseconds = null)
{
serializer ??= _defaultSerializer;
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 (Options.OutputOriginalData == true || _log.Level == LogLevel.Trace)
if (OutputOriginalData == true)
{
data = await reader.ReadToEndAsync().ConfigureAwait(false);
_log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] " : "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms{(_log.Level == LogLevel.Trace ? (": " + data) : "")}");
var result = Deserialize<T>(data, serializer, requestId);
if (Options.OutputOriginalData == true)
result.OriginalData = data;
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);
_log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] " : "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms");
return new CallResult<T>(serializer.Deserialize<T>(jsonReader)!);
}
catch (JsonReaderException jre)
@@ -284,7 +274,7 @@ namespace CryptoExchange.Net
data = "[Data only available in Trace LogLevel]";
}
}
_log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}");
_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)
@@ -302,7 +292,7 @@ namespace CryptoExchange.Net
}
}
_log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}");
_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)
@@ -321,7 +311,7 @@ namespace CryptoExchange.Net
}
var exceptionInfo = ex.ToLogString();
_log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}");
_logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}");
return new CallResult<T>(new DeserializeError($"Deserialize Unknown Exception: {exceptionInfo}", data));
}
}
@@ -332,27 +322,13 @@ namespace CryptoExchange.Net
return await reader.ReadToEndAsync().ConfigureAwait(false);
}
/// <summary>
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique across different client instances
/// </summary>
/// <returns></returns>
protected static int NextId()
{
lock (idLock)
{
lastId += 1;
return lastId;
}
}
/// <summary>
/// Dispose
/// </summary>
public virtual void Dispose()
{
_disposing = true;
_apiCredentials?.Dispose();
AuthenticationProvider?.Credentials?.Dispose();
AuthenticationProvider?.Dispose();
}
}
}
+29 -25
View File
@@ -1,7 +1,7 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.Collections.Generic;
@@ -16,37 +16,48 @@ namespace CryptoExchange.Net
/// The name of the API the client is for
/// </summary>
internal string Name { get; }
/// <summary>
/// Api clients in this client
/// </summary>
internal List<BaseApiClient> ApiClients { get; } = new List<BaseApiClient>();
/// <summary>
/// The log object
/// </summary>
protected internal Log log;
protected internal ILogger _logger;
/// <summary>
/// Provided client options
/// </summary>
public ClientOptions ClientOptions { get; }
public ExchangeOptions ClientOptions { get; private set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="logger">Logger</param>
/// <param name="name">The name of the API this client is for</param>
/// <param name="options">The options for this client</param>
protected BaseClient(string name, ClientOptions options)
#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)
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
{
log = new Log(name);
log.UpdateWriters(options.LogWriters);
log.Level = options.LogLevel;
options.OnLoggingChanged += HandleLogConfigChange;
ClientOptions = options;
_logger = logger?.CreateLogger(name) ?? NullLoggerFactory.Instance.CreateLogger(name);
Name = name;
}
log.Write(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {name}.Net: v{GetType().Assembly.GetName().Version}");
/// <summary>
/// Initialize the client with the specified options
/// </summary>
/// <param name="options"></param>
/// <exception cref="ArgumentNullException"></exception>
protected virtual void Initialize(ExchangeOptions options)
{
if (options == null)
throw new ArgumentNullException(nameof(options));
ClientOptions = options;
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {Name}.Net: v{GetType().Assembly.GetName().Version}");
}
/// <summary>
@@ -65,27 +76,20 @@ namespace CryptoExchange.Net
/// <param name="apiClient">The client</param>
protected T AddApiClient<T>(T apiClient) where T: BaseApiClient
{
log.Write(LogLevel.Trace, $" {apiClient.GetType().Name} configuration: {apiClient.Options}");
if (ClientOptions == null)
throw new InvalidOperationException("Client should have called Initialize before adding API clients");
_logger.Log(LogLevel.Trace, $" {apiClient.GetType().Name}, base address: {apiClient.BaseAddress}");
ApiClients.Add(apiClient);
return apiClient;
}
/// <summary>
/// Handle a change in the client options log config
/// </summary>
private void HandleLogConfigChange()
{
log.UpdateWriters(ClientOptions.LogWriters);
log.Level = ClientOptions.LogLevel;
}
/// <summary>
/// Dispose
/// </summary>
public virtual void Dispose()
{
log.Write(LogLevel.Debug, "Disposing client");
ClientOptions.OnLoggingChanged -= HandleLogConfigChange;
_logger.Log(LogLevel.Debug, "Disposing client");
foreach (var client in ApiClients)
client.Dispose();
}
+3 -7
View File
@@ -1,8 +1,6 @@
using System;
using System.Linq;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net
{
@@ -17,12 +15,10 @@ namespace CryptoExchange.Net
/// <summary>
/// ctor
/// </summary>
/// <param name="loggerFactory">Logger factory</param>
/// <param name="name">The name of the API this client is for</param>
/// <param name="options">The options for this client</param>
protected BaseRestClient(string name, ClientOptions options) : base(name, options)
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
{
if (options == null)
throw new ArgumentNullException(nameof(options));
}
}
}
+10 -10
View File
@@ -3,10 +3,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net
@@ -21,7 +19,7 @@ namespace CryptoExchange.Net
/// <summary>
/// If client is disposing
/// </summary>
protected bool disposing;
protected bool _disposing;
/// <inheritdoc />
public int CurrentConnections => ApiClients.OfType<SocketApiClient>().Sum(c => c.CurrentConnections);
@@ -34,9 +32,9 @@ namespace CryptoExchange.Net
/// <summary>
/// ctor
/// </summary>
/// <param name="logger">Logger</param>
/// <param name="name">The name of the API this client is for</param>
/// <param name="options">The options for this client</param>
protected BaseSocketClient(string name, ClientOptions options) : base(name, options)
protected BaseSocketClient(ILoggerFactory? logger, string name) : base(logger, name)
{
}
@@ -65,7 +63,7 @@ namespace CryptoExchange.Net
if (subscription == null)
throw new ArgumentNullException(nameof(subscription));
log.Write(LogLevel.Information, $"Socket {subscription.SocketId} Unsubscribing subscription " + subscription.Id);
_logger.Log(LogLevel.Information, $"Socket {subscription.SocketId} Unsubscribing subscription " + subscription.Id);
await subscription.CloseAsync().ConfigureAwait(false);
}
@@ -88,7 +86,7 @@ namespace CryptoExchange.Net
/// <returns></returns>
public virtual async Task ReconnectAsync()
{
log.Write(LogLevel.Information, $"Reconnecting all {CurrentConnections} connections");
_logger.Log(LogLevel.Information, $"Reconnecting all {CurrentConnections} connections");
var tasks = new List<Task>();
foreach (var client in ApiClients.OfType<SocketApiClient>())
{
@@ -103,8 +101,10 @@ namespace CryptoExchange.Net
public string GetSubscriptionsState()
{
var result = new StringBuilder();
foreach(var client in ApiClients.OfType<SocketApiClient>())
result.AppendLine(client.GetSubscriptionsState());
foreach (var client in ApiClients.OfType<SocketApiClient>().Where(c => c.CurrentSubscriptions > 0))
{
result.AppendLine(client.GetSubscriptionsState());
}
return result.ToString();
}
}
@@ -0,0 +1,67 @@
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.Clients
{
/// <summary>
/// Base crypto client
/// </summary>
public class CryptoBaseClient : IDisposable
{
private Dictionary<Type, object> _serviceCache = new Dictionary<Type, object>();
/// <summary>
/// Service provider
/// </summary>
protected readonly IServiceProvider? _serviceProvider;
/// <summary>
/// ctor
/// </summary>
public CryptoBaseClient() { }
/// <summary>
/// ctor
/// </summary>
/// <param name="serviceProvider"></param>
public CryptoBaseClient(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_serviceCache = new Dictionary<Type, object>();
}
/// <summary>
/// Try get a client by type for the service collection
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T TryGet<T>(Func<T> createFunc)
{
var type = typeof(T);
if (_serviceCache.TryGetValue(type, out var value))
return (T)value;
if (_serviceProvider == null)
{
// Create with default options
var createResult = createFunc();
_serviceCache.Add(typeof(T), createResult!);
return createResult;
}
var result = _serviceProvider.GetService<T>()
?? throw new InvalidOperationException($"No service was found for {typeof(T).Name}, make sure the exchange is registered in dependency injection with the `services.Add[Exchange]()` method");
_serviceCache.Add(type, result!);
return result;
}
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
_serviceCache.Clear();
}
}
}
@@ -0,0 +1,47 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Interfaces.CommonClients;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
namespace CryptoExchange.Net.Clients
{
/// <inheritdoc />
public class CryptoRestClient : CryptoBaseClient, ICryptoRestClient
{
/// <summary>
/// ctor
/// </summary>
public CryptoRestClient()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="serviceProvider"></param>
public CryptoRestClient(IServiceProvider serviceProvider) : base(serviceProvider)
{
}
/// <summary>
/// Get a list of the registered ISpotClient implementations
/// </summary>
/// <returns></returns>
public IEnumerable<ISpotClient> GetSpotClients()
{
if (_serviceProvider == null)
return new List<ISpotClient>();
return _serviceProvider.GetServices<ISpotClient>().ToList();
}
/// <summary>
/// Get an ISpotClient implementation by exchange name
/// </summary>
/// <param name="exchangeName"></param>
/// <returns></returns>
public ISpotClient? SpotClient(string exchangeName) => _serviceProvider.GetServices<ISpotClient>()?.SingleOrDefault(s => s.ExchangeName.Equals(exchangeName, StringComparison.InvariantCultureIgnoreCase));
}
}
@@ -0,0 +1,24 @@
using CryptoExchange.Net.Interfaces;
using System;
namespace CryptoExchange.Net.Clients
{
/// <inheritdoc />
public class CryptoSocketClient : CryptoBaseClient, ICryptoSocketClient
{
/// <summary>
/// ctor
/// </summary>
public CryptoSocketClient()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="serviceProvider"></param>
public CryptoSocketClient(IServiceProvider serviceProvider) : base(serviceProvider)
{
}
}
}
+105 -54
View File
@@ -4,12 +4,13 @@ using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Requests;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
@@ -24,6 +25,7 @@ namespace CryptoExchange.Net
{
/// <inheritdoc />
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
/// <inheritdoc />
public abstract TimeSyncInfo? GetTimeSyncInfo();
@@ -38,36 +40,39 @@ namespace CryptoExchange.Net
/// </summary>
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
/// <summary>
/// Options for this client
/// </summary>
public new RestApiClientOptions Options => (RestApiClientOptions)base.Options;
/// <summary>
/// List of rate limiters
/// </summary>
internal IEnumerable<IRateLimiter> RateLimiters { get; }
/// <summary>
/// Options
/// </summary>
internal ClientOptions ClientOptions { get; set; }
/// <inheritdoc />
public new RestExchangeOptions ClientOptions => (RestExchangeOptions)base.ClientOptions;
/// <inheritdoc />
public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions;
/// <summary>
/// ctor
/// </summary>
/// <param name="log">Logger</param>
/// <param name="logger">Logger</param>
/// <param name="httpClient">HttpClient to use</param>
/// <param name="baseAddress">Base address for this API client</param>
/// <param name="options">The base client options</param>
/// <param name="apiOptions">The Api client options</param>
public RestApiClient(Log log, ClientOptions options, RestApiClientOptions apiOptions) : base(log, options, apiOptions)
public RestApiClient(ILogger logger, HttpClient? httpClient, string baseAddress, RestExchangeOptions options, RestApiOptions apiOptions)
: base(logger,
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
apiOptions.ApiCredentials ?? options.ApiCredentials,
baseAddress,
options,
apiOptions)
{
var rateLimiters = new List<IRateLimiter>();
foreach (var rateLimiter in apiOptions.RateLimiters)
rateLimiters.Add(rateLimiter);
RateLimiters = rateLimiters;
ClientOptions = options;
RequestFactory.Configure(apiOptions.RequestTimeout, options.Proxy, apiOptions.HttpClient);
RequestFactory.Configure(options.Proxy, options.RequestTimeout, httpClient);
}
/// <summary>
@@ -78,6 +83,7 @@ namespace CryptoExchange.Net
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="parameters">The parameters of the request</param>
/// <param name="signed">Whether or not the request should be authenticated</param>
/// <param name="requestBodyFormat">The format of the body content</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="requestWeight">Credits used for the request</param>
@@ -92,6 +98,7 @@ namespace CryptoExchange.Net
CancellationToken cancellationToken,
Dictionary<string, object>? parameters = null,
bool signed = false,
RequestBodyFormat? requestBodyFormat = null,
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null,
int requestWeight = 1,
@@ -103,11 +110,16 @@ namespace CryptoExchange.Net
while (true)
{
currentTry++;
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
if (!request)
return new WebCallResult(request.Error!);
var result = await GetResponseAsync<object>(request.Data, deserializer, cancellationToken, true).ConfigureAwait(false);
if (!result)
_logger.Log(LogLevel.Warning, $"[Req {result.RequestId}] {result.ResponseStatusCode} Error received in {result.ResponseTime!.Value.TotalMilliseconds}ms: {result.Error}");
else
_logger.Log(LogLevel.Debug, $"[Req {result.RequestId}] {result.ResponseStatusCode} Response received in {result.ResponseTime!.Value.TotalMilliseconds}ms{(OutputOriginalData ? (": " + result.OriginalData) : "")}");
if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false))
continue;
@@ -124,6 +136,7 @@ namespace CryptoExchange.Net
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="parameters">The parameters of the request</param>
/// <param name="signed">Whether or not the request should be authenticated</param>
/// <param name="requestBodyFormat">The format of the body content</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="requestWeight">Credits used for the request</param>
@@ -138,6 +151,7 @@ namespace CryptoExchange.Net
CancellationToken cancellationToken,
Dictionary<string, object>? parameters = null,
bool signed = false,
RequestBodyFormat? requestBodyFormat = null,
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null,
int requestWeight = 1,
@@ -150,11 +164,16 @@ namespace CryptoExchange.Net
while (true)
{
currentTry++;
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
if (!request)
return new WebCallResult<T>(request.Error!);
var result = await GetResponseAsync<T>(request.Data, deserializer, cancellationToken, false).ConfigureAwait(false);
if (!result)
_logger.Log(LogLevel.Warning, $"[Req {result.RequestId}] {result.ResponseStatusCode} Error received in {result.ResponseTime!.Value.TotalMilliseconds}ms: {result.Error}");
else
_logger.Log(LogLevel.Debug, $"[Req {result.RequestId}] {result.ResponseStatusCode} Response received in {result.ResponseTime!.Value.TotalMilliseconds}ms{(OutputOriginalData ? (": " + result.OriginalData) : "")}");
if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false))
continue;
@@ -170,6 +189,7 @@ namespace CryptoExchange.Net
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="parameters">The parameters of the request</param>
/// <param name="signed">Whether or not the request should be authenticated</param>
/// <param name="requestBodyFormat">The format of the body content</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="requestWeight">Credits used for the request</param>
@@ -183,6 +203,7 @@ namespace CryptoExchange.Net
CancellationToken cancellationToken,
Dictionary<string, object>? parameters = null,
bool signed = false,
RequestBodyFormat? requestBodyFormat = null,
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null,
int requestWeight = 1,
@@ -190,7 +211,7 @@ namespace CryptoExchange.Net
Dictionary<string, string>? additionalHeaders = null,
bool ignoreRatelimit = false)
{
var requestId = NextId();
var requestId = ExchangeHelpers.NextId();
if (signed)
{
@@ -203,7 +224,7 @@ namespace CryptoExchange.Net
var syncTimeResult = await syncTask.ConfigureAwait(false);
if (!syncTimeResult)
{
_log.Write(LogLevel.Debug, $"[{requestId}] Failed to sync time, aborting request: " + syncTimeResult.Error);
_logger.Log(LogLevel.Debug, $"[Req {requestId}] Failed to sync time, aborting request: " + syncTimeResult.Error);
return syncTimeResult.As<IRequest>(default);
}
}
@@ -213,7 +234,7 @@ namespace CryptoExchange.Net
{
foreach (var limiter in RateLimiters)
{
var limitResult = await limiter.LimitRequestAsync(_log, uri.AbsolutePath, method, signed, Options.ApiCredentials?.Key ?? ClientOptions.ApiCredentials?.Key, Options.RateLimitingBehaviour, requestWeight, cancellationToken).ConfigureAwait(false);
var limitResult = await limiter.LimitRequestAsync(_logger, uri.AbsolutePath, method, signed, ApiOptions.ApiCredentials?.Key ?? ClientOptions.ApiCredentials?.Key, ApiOptions.RateLimitingBehaviour, requestWeight, cancellationToken).ConfigureAwait(false);
if (!limitResult.Success)
return new CallResult<IRequest>(limitResult.Error!);
}
@@ -221,13 +242,13 @@ namespace CryptoExchange.Net
if (signed && AuthenticationProvider == null)
{
_log.Write(LogLevel.Warning, $"[{requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
_logger.Log(LogLevel.Warning, $"[Req {requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
return new CallResult<IRequest>(new NoApiCredentialsError());
}
_log.Write(LogLevel.Information, $"[{requestId}] Creating request for " + uri);
_logger.Log(LogLevel.Information, $"[Req {requestId}] Creating request for " + uri);
var paramsPosition = parameterPosition ?? ParameterPositions[method];
var request = ConstructRequest(uri, method, parameters?.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value), signed, paramsPosition, arraySerialization ?? this.arraySerialization, requestId, additionalHeaders);
var request = ConstructRequest(uri, method, parameters?.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value), signed, paramsPosition, arraySerialization ?? this.arraySerialization, requestBodyFormat ?? this.requestBodyFormat, requestId, additionalHeaders);
string? paramString = "";
if (paramsPosition == HttpMethodParameterPosition.InBody)
@@ -238,7 +259,7 @@ namespace CryptoExchange.Net
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
TotalRequestsMade++;
_log.Write(LogLevel.Trace, $"[{requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(ClientOptions.Proxy == null ? "" : $" via proxy {ClientOptions.Proxy.Host}")}");
_logger.Log(LogLevel.Trace, $"[Req {requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}");
return new CallResult<IRequest>(request);
}
@@ -256,41 +277,42 @@ namespace CryptoExchange.Net
CancellationToken cancellationToken,
bool expectedEmptyResponse)
{
var sw = Stopwatch.StartNew();
try
{
var sw = Stopwatch.StartNew();
var response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
sw.Stop();
var statusCode = response.StatusCode;
var headers = response.ResponseHeaders;
var responseLength = response.ContentLength;
var responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
// If we have to manually parse error responses (can't rely on HttpStatusCode) we'll need to read the full
// response before being able to deserialize it into the resulting type since we don't know if it an error response or data
// response before being able to deserialize it into the resulting type since we don't know if its an error response or data
if (manualParseError)
{
using var reader = new StreamReader(responseStream);
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
responseLength ??= data.Length;
responseStream.Close();
response.Close();
_log.Write(LogLevel.Debug, $"[{request.RequestId}] Response received in {sw.ElapsedMilliseconds}ms{(_log.Level == LogLevel.Trace ? (": " + data) : "")}");
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, Options.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
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, Options.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
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, Options.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error);
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
{
@@ -299,16 +321,16 @@ namespace CryptoExchange.Net
var parseResult = ValidateJson(data);
if (!parseResult.Success)
// Not empty, and not json
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, Options.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
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, Options.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
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, Options.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, default);
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
@@ -319,7 +341,7 @@ namespace CryptoExchange.Net
responseStream.Close();
response.Close();
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, null);
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
@@ -327,7 +349,7 @@ namespace CryptoExchange.Net
responseStream.Close();
response.Close();
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, Options.OutputOriginalData ? desResult.OriginalData : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), desResult.Data, desResult.Error);
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
@@ -335,36 +357,37 @@ namespace CryptoExchange.Net
// Http status code indicates error
using var reader = new StreamReader(responseStream);
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
_log.Write(LogLevel.Warning, $"[{request.RequestId}] Error received in {sw.ElapsedMilliseconds}ms: {data}");
responseStream.Close();
response.Close();
var parseResult = ValidateJson(data);
var error = parseResult.Success ? ParseErrorResponse(parseResult.Data) : new ServerError(data)!;
Error error;
if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429)
error = ParseRateLimitResponse((int)response.StatusCode, response.ResponseHeaders, data);
else
error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, data);
if (error.Code == null || error.Code == 0)
error.Code = (int)response.StatusCode;
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data.Length, data, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
}
}
catch (HttpRequestException requestException)
{
// Request exception, can't reach server for instance
var exceptionInfo = requestException.ToLogString();
_log.Write(LogLevel.Warning, $"[{request.RequestId}] Request exception: " + exceptionInfo);
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError(exceptionInfo));
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError(exceptionInfo));
}
catch (OperationCanceledException canceledException)
{
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
{
// Cancellation token canceled by caller
_log.Write(LogLevel.Warning, $"[{request.RequestId}] Request canceled by cancellation token");
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError());
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError());
}
else
{
// Request timed out
_log.Write(LogLevel.Warning, $"[{request.RequestId}] Request timed out: " + canceledException.ToLogString());
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError($"[{request.RequestId}] 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"));
}
}
}
@@ -400,6 +423,7 @@ namespace CryptoExchange.Net
/// <param name="signed">Whether or not the request should be authenticated</param>
/// <param name="parameterPosition">Where the parameters should be placed</param>
/// <param name="arraySerialization">How array parameters should be serialized</param>
/// <param name="bodyFormat">Format of the body content</param>
/// <param name="requestId">Unique id of a request</param>
/// <param name="additionalHeaders">Additional headers to send with the request</param>
/// <returns></returns>
@@ -410,6 +434,7 @@ namespace CryptoExchange.Net
bool signed,
HttpMethodParameterPosition parameterPosition,
ArrayParametersSerialization arraySerialization,
RequestBodyFormat bodyFormat,
int requestId,
Dictionary<string, string>? additionalHeaders)
{
@@ -490,7 +515,7 @@ namespace CryptoExchange.Net
if (parameterPosition == HttpMethodParameterPosition.InBody)
{
var contentType = requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
if (bodyParameters.Any())
WriteParamBody(request, bodyParameters, contentType);
else
@@ -508,13 +533,13 @@ namespace CryptoExchange.Net
/// <param name="contentType">The content type of the data</param>
protected virtual void WriteParamBody(IRequest request, SortedDictionary<string, object> parameters, string contentType)
{
if (requestBodyFormat == RequestBodyFormat.Json)
if (contentType == Constants.JsonContentHeader)
{
// Write the parameters as json in the body
var stringData = JsonConvert.SerializeObject(parameters);
request.SetContent(stringData, contentType);
}
else if (requestBodyFormat == RequestBodyFormat.FormData)
else if (contentType == Constants.FormContentHeader)
{
// Write the parameters as form data in the body
var stringData = parameters.ToFormData();
@@ -523,13 +548,39 @@ namespace CryptoExchange.Net
}
/// <summary>
/// Parse an error response from the server. Only used when server returns a status other than Success(200)
/// Parse an error response from the server. Only used when server returns a status other than Success(200) or ratelimit error (429 or 418)
/// </summary>
/// <param name="error">The string the request returned</param>
/// <param name="httpStatusCode">The response status code</param>
/// <param name="responseHeaders">The response headers</param>
/// <param name="data">The response data</param>
/// <returns></returns>
protected virtual Error ParseErrorResponse(JToken error)
protected virtual Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, string data)
{
return new ServerError(error.ToString());
return new ServerError(data);
}
/// <summary>
/// Parse a rate limit error response from the server. Only used when server returns http status 429 or 418
/// </summary>
/// <param name="httpStatusCode">The response status code</param>
/// <param name="responseHeaders">The response headers</param>
/// <param name="data">The response data</param>
/// <returns></returns>
protected virtual Error ParseRateLimitResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, string data)
{
// Handle retry after header
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
if (retryAfterHeader.Value?.Any() != true)
return new ServerRateLimitError(data);
var value = retryAfterHeader.Value.First();
if (int.TryParse(value, out var seconds))
return new ServerRateLimitError(data) { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) };
if (DateTime.TryParse(value, out var datetime))
return new ServerRateLimitError(data) { RetryAfter = datetime };
return new ServerRateLimitError(data);
}
/// <summary>
@@ -542,14 +593,14 @@ namespace CryptoExchange.Net
{
var timeSyncParams = GetTimeSyncInfo();
if (timeSyncParams == null)
return new WebCallResult<bool>(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);
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
{
if (!timeSyncParams.SyncTime || (DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval))
{
timeSyncParams.TimeSyncState.Semaphore.Release();
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, true, null);
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
}
var localTime = DateTime.UtcNow;
@@ -578,7 +629,7 @@ namespace CryptoExchange.Net
timeSyncParams.TimeSyncState.Semaphore.Release();
}
return new WebCallResult<bool>(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);
}
}
}
+207 -342
View File
@@ -1,14 +1,18 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -28,40 +32,21 @@ namespace CryptoExchange.Net
/// List of socket connections currently connecting/connected
/// </summary>
protected internal ConcurrentDictionary<int, SocketConnection> socketConnections = new();
/// <summary>
/// Semaphore used while creating sockets
/// </summary>
protected internal readonly SemaphoreSlim semaphoreSlim = new(1);
/// <summary>
/// Keep alive interval for websocket connection
/// </summary>
protected TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10);
/// <summary>
/// Delegate used for processing byte data received from socket connections before it is processed by handlers
/// </summary>
protected Func<byte[], string>? dataInterpreterBytes;
/// <summary>
/// Delegate used for processing string data received from socket connections before it is processed by handlers
/// </summary>
protected Func<string, string>? dataInterpreterString;
/// <summary>
/// Handlers for data from the socket which doesn't need to be forwarded to the caller. Ping or welcome messages for example.
/// </summary>
protected Dictionary<string, Action<MessageEvent>> genericHandlers = new();
/// <summary>
/// The task that is sending periodic data on the websocket. Can be used for sending Ping messages every x seconds or similair. Not necesarry.
/// </summary>
protected Task? periodicTask;
/// <summary>
/// Wait event for the periodicTask
/// </summary>
protected AsyncResetEvent? periodicEvent;
/// <summary>
/// If true; data which is a response to a query will also be distributed to subscriptions
/// If false; data which is a response to a query won't get forwarded to subscriptions as well
/// </summary>
protected internal bool ContinueOnQueryResponse { get; protected set; }
protected List<SystemSubscription> systemSubscriptions = new();
/// <summary>
/// If a message is received on the socket which is not handled by a handler this boolean determines whether this logs an error message
@@ -69,9 +54,19 @@ namespace CryptoExchange.Net
protected internal bool UnhandledMessageExpected { get; set; }
/// <summary>
/// The max amount of outgoing messages per socket per second
/// If true a subscription will accept message before the confirmation of a subscription has been received
/// </summary>
protected internal int? RateLimitPerSocketPerSecond { get; set; }
protected bool HandleMessageBeforeConfirmation { get; set; }
/// <summary>
/// The rate limiters
/// </summary>
protected internal IEnumerable<IRateLimiter>? RateLimiters { get; set; }
/// <summary>
/// Periodic task regisrations
/// </summary>
protected List<PeriodicTaskRegistration> PeriodicTaskRegistrations { get; set; } = new List<PeriodicTaskRegistration>();
/// <inheritdoc />
public double IncomingKbps
@@ -87,6 +82,7 @@ namespace CryptoExchange.Net
/// <inheritdoc />
public int CurrentConnections => socketConnections.Count;
/// <inheritdoc />
public int CurrentSubscriptions
{
@@ -95,74 +91,84 @@ namespace CryptoExchange.Net
if (!socketConnections.Any())
return 0;
return socketConnections.Sum(s => s.Value.SubscriptionCount);
return socketConnections.Sum(s => s.Value.UserSubscriptionCount);
}
}
/// <inheritdoc />
public new SocketApiClientOptions Options => (SocketApiClientOptions)base.Options;
public new SocketExchangeOptions ClientOptions => (SocketExchangeOptions)base.ClientOptions;
/// <inheritdoc />
public new SocketApiOptions ApiOptions => (SocketApiOptions)base.ApiOptions;
/// <summary>
/// Options
/// </summary>
internal ClientOptions ClientOptions { get; set; }
#endregion
/// <summary>
/// ctor
/// </summary>
/// <param name="log">log</param>
/// <param name="logger">log</param>
/// <param name="options">Client options</param>
/// <param name="baseAddress">Base address for this API client</param>
/// <param name="apiOptions">The Api client options</param>
public SocketApiClient(Log log, ClientOptions options, SocketApiClientOptions apiOptions) : base(log, options, apiOptions)
public SocketApiClient(ILogger logger, string baseAddress, SocketExchangeOptions options, SocketApiOptions apiOptions)
: base(logger,
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
apiOptions.ApiCredentials ?? options.ApiCredentials,
baseAddress,
options,
apiOptions)
{
ClientOptions = options;
var rateLimiters = new List<IRateLimiter>();
foreach (var rateLimiter in apiOptions.RateLimiters)
rateLimiters.Add(rateLimiter);
RateLimiters = rateLimiters;
}
/// <summary>
/// Set a delegate to be used for processing data received from socket connections before it is processed by handlers
/// Add a query to periodically send on each connection
/// </summary>
/// <param name="byteHandler">Handler for byte data</param>
/// <param name="stringHandler">Handler for string data</param>
protected void SetDataInterpreter(Func<byte[], string>? byteHandler, Func<string, string>? stringHandler)
/// <param name="identifier"></param>
/// <param name="interval"></param>
/// <param name="queryDelegate"></param>
/// <param name="callback"></param>
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<CallResult>? callback)
{
dataInterpreterBytes = byteHandler;
dataInterpreterString = stringHandler;
PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration
{
Identifier = identifier,
Callback = callback,
Interval = interval,
QueryDelegate = queryDelegate
});
}
/// <summary>
/// Connect to an url and listen for data on the BaseAddress
/// </summary>
/// <typeparam name="T">The type of the expected data</typeparam>
/// <param name="request">The optional request object to send, will be serialized to json</param>
/// <param name="identifier">The identifier to use, necessary if no request object is sent</param>
/// <param name="authenticated">If the subscription is to an authenticated endpoint</param>
/// <param name="dataHandler">The handler of update data</param>
/// <param name="subscription">The subscription</param>
/// <param name="ct">Cancellation token for closing this subscription</param>
/// <returns></returns>
protected virtual Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler, CancellationToken ct)
protected virtual Task<CallResult<UpdateSubscription>> SubscribeAsync(Subscription subscription, CancellationToken ct)
{
return SubscribeAsync(Options.BaseAddress, request, identifier, authenticated, dataHandler, ct);
return SubscribeAsync(BaseAddress, subscription, ct);
}
/// <summary>
/// Connect to an url and listen for data
/// </summary>
/// <typeparam name="T">The type of the expected data</typeparam>
/// <param name="url">The URL to connect to</param>
/// <param name="request">The optional request object to send, will be serialized to json</param>
/// <param name="identifier">The identifier to use, necessary if no request object is sent</param>
/// <param name="authenticated">If the subscription is to an authenticated endpoint</param>
/// <param name="dataHandler">The handler of update data</param>
/// <param name="subscription">The subscription</param>
/// <param name="ct">Cancellation token for closing this subscription</param>
/// <returns></returns>
protected virtual async Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(string url, object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler, CancellationToken ct)
protected virtual async Task<CallResult<UpdateSubscription>> SubscribeAsync(string url, Subscription subscription, CancellationToken ct)
{
if (_disposing)
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
if (subscription.Authenticated && AuthenticationProvider == null)
return new CallResult<UpdateSubscription>(new NoApiCredentialsError());
SocketConnection socketConnection;
SocketSubscription? subscription;
var released = false;
// Wait for a semaphore here, so we only connect 1 socket at a time.
// This is necessary for being able to see if connections can be combined
@@ -180,21 +186,21 @@ namespace CryptoExchange.Net
while (true)
{
// Get a new or existing socket connection
var socketResult = await GetSocketConnection(url, authenticated).ConfigureAwait(false);
var socketResult = await GetSocketConnection(url, subscription.Authenticated).ConfigureAwait(false);
if (!socketResult)
return socketResult.As<UpdateSubscription>(null);
socketConnection = socketResult.Data;
// Add a subscription on the socket connection
subscription = AddSubscription(request, identifier, true, socketConnection, dataHandler, authenticated);
if (subscription == null)
var success = socketConnection.CanAddSubscription();
if (!success)
{
_log.Write(LogLevel.Trace, $"Socket {socketConnection.SocketId} failed to add subscription, retrying on different connection");
_logger.Log(LogLevel.Trace, $"[Sckt {socketConnection.SocketId}] failed to add subscription, retrying on different connection");
continue;
}
if (Options.SocketSubscriptionsCombineTarget == 1)
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
{
// Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway
semaphoreSlim.Release();
@@ -203,7 +209,7 @@ namespace CryptoExchange.Net
var needsConnecting = !socketConnection.Connected;
var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false);
var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated).ConfigureAwait(false);
if (!connectResult)
return new CallResult<UpdateSubscription>(connectResult.Error!);
@@ -218,74 +224,60 @@ namespace CryptoExchange.Net
if (socketConnection.PausedActivity)
{
_log.Write(LogLevel.Warning, $"Socket {socketConnection.SocketId} has been paused, can't subscribe at this moment");
_logger.Log(LogLevel.Warning, $"[Sckt {socketConnection.SocketId}] has been paused, can't subscribe at this moment");
return new CallResult<UpdateSubscription>(new ServerError("Socket is paused"));
}
if (request != null)
var waitEvent = new AsyncResetEvent(false);
var subQuery = subscription.GetSubQuery(socketConnection);
if (subQuery != null)
{
if (HandleMessageBeforeConfirmation)
socketConnection.AddSubscription(subscription);
// Send the request and wait for answer
var subResult = await SubscribeAndWaitAsync(socketConnection, request, subscription).ConfigureAwait(false);
var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, waitEvent).ConfigureAwait(false);
if (!subResult)
{
_log.Write(LogLevel.Warning, $"Socket {socketConnection.SocketId} failed to subscribe: {subResult.Error}");
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
waitEvent?.Set();
_logger.Log(LogLevel.Warning, $"[Sckt {socketConnection.SocketId}] failed to subscribe: {subResult.Error}");
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
var unsubscribe = subResult.Error is CancellationRequestedError;
await socketConnection.CloseAsync(subscription, unsubscribe).ConfigureAwait(false);
return new CallResult<UpdateSubscription>(subResult.Error!);
}
}
else
{
// No request to be sent, so just mark the subscription as comfirmed
subscription.Confirmed = true;
subscription.HandleSubQueryResponse(subQuery.Response!);
}
subscription.Confirmed = true;
if (ct != default)
{
subscription.CancellationTokenRegistration = ct.Register(async () =>
{
_log.Write(LogLevel.Information, $"Socket {socketConnection.SocketId} Cancellation token set, closing subscription");
_logger.Log(LogLevel.Information, $"[Sckt {socketConnection.SocketId}] Cancellation token set, closing subscription {subscription.Id}");
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
}, false);
}
_log.Write(LogLevel.Information, $"Socket {socketConnection.SocketId} subscription {subscription.Id} completed successfully");
if (!HandleMessageBeforeConfirmation)
socketConnection.AddSubscription(subscription);
waitEvent?.Set();
_logger.Log(LogLevel.Information, $"[Sckt {socketConnection.SocketId}] subscription {subscription.Id} completed successfully");
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
}
/// <summary>
/// Sends the subscribe request and waits for a response to that request
/// </summary>
/// <param name="socketConnection">The connection to send the request on</param>
/// <param name="request">The request to send, will be serialized to json</param>
/// <param name="subscription">The subscription the request is for</param>
/// <returns></returns>
protected internal virtual async Task<CallResult<bool>> SubscribeAndWaitAsync(SocketConnection socketConnection, object request, SocketSubscription subscription)
{
CallResult<object>? callResult = null;
await socketConnection.SendAndWaitAsync(request, Options.SocketResponseTimeout, subscription, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false);
if (callResult?.Success == true)
{
subscription.Confirmed = true;
return new CallResult<bool>(true);
}
if (callResult == null)
return new CallResult<bool>(new ServerError("No response on subscription request received"));
return new CallResult<bool>(callResult.Error!);
}
/// <summary>
/// Send a query on a socket connection to the BaseAddress and wait for the response
/// </summary>
/// <typeparam name="T">Expected result type</typeparam>
/// <param name="request">The request to send, will be serialized to json</param>
/// <param name="authenticated">If the query is to an authenticated endpoint</param>
/// <param name="query">The query</param>
/// <returns></returns>
protected virtual Task<CallResult<T>> QueryAsync<T>(object request, bool authenticated)
protected virtual Task<CallResult<T>> QueryAsync<T>(Query<T> query)
{
return QueryAsync<T>(Options.BaseAddress, request, authenticated);
return QueryAsync(BaseAddress, query);
}
/// <summary>
@@ -293,10 +285,9 @@ namespace CryptoExchange.Net
/// </summary>
/// <typeparam name="T">The expected result type</typeparam>
/// <param name="url">The url for the request</param>
/// <param name="request">The request to send</param>
/// <param name="authenticated">Whether the socket should be authenticated</param>
/// <param name="query">The query</param>
/// <returns></returns>
protected virtual async Task<CallResult<T>> QueryAsync<T>(string url, object request, bool authenticated)
protected virtual async Task<CallResult<T>> QueryAsync<T>(string url, Query<T> query)
{
if (_disposing)
return new CallResult<T>(new InvalidOperationError("Client disposed, can't query"));
@@ -306,20 +297,20 @@ namespace CryptoExchange.Net
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
try
{
var socketResult = await GetSocketConnection(url, authenticated).ConfigureAwait(false);
var socketResult = await GetSocketConnection(url, query.Authenticated).ConfigureAwait(false);
if (!socketResult)
return socketResult.As<T>(default);
socketConnection = socketResult.Data;
if (Options.SocketSubscriptionsCombineTarget == 1)
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
{
// Can release early when only a single sub per connection
semaphoreSlim.Release();
released = true;
}
var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false);
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated).ConfigureAwait(false);
if (!connectResult)
return new CallResult<T>(connectResult.Error!);
}
@@ -331,33 +322,11 @@ namespace CryptoExchange.Net
if (socketConnection.PausedActivity)
{
_log.Write(LogLevel.Warning, $"Socket {socketConnection.SocketId} has been paused, can't send query at this moment");
_logger.Log(LogLevel.Warning, $"[Sckt {socketConnection.SocketId}] has been paused, can't send query at this moment");
return new CallResult<T>(new ServerError("Socket is paused"));
}
return await QueryAndWaitAsync<T>(socketConnection, request).ConfigureAwait(false);
}
/// <summary>
/// Sends the query request and waits for the result
/// </summary>
/// <typeparam name="T">The expected result type</typeparam>
/// <param name="socket">The connection to send and wait on</param>
/// <param name="request">The request to send</param>
/// <returns></returns>
protected virtual async Task<CallResult<T>> QueryAndWaitAsync<T>(SocketConnection socket, object request)
{
var dataResult = new CallResult<T>(new ServerError("No response on query received"));
await socket.SendAndWaitAsync(request, Options.SocketResponseTimeout, null, data =>
{
if (!HandleQueryResponse<T>(socket, request, data, out var callResult))
return false;
dataResult = callResult;
return true;
}).ConfigureAwait(false);
return dataResult;
return await socketConnection.SendAndWaitQueryAsync(query).ConfigureAwait(false);
}
/// <summary>
@@ -375,149 +344,62 @@ namespace CryptoExchange.Net
if (!connectResult)
return new CallResult<bool>(connectResult.Error!);
if (Options.DelayAfterConnect != TimeSpan.Zero)
await Task.Delay(Options.DelayAfterConnect).ConfigureAwait(false);
if (ClientOptions.DelayAfterConnect != TimeSpan.Zero)
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
if (!authenticated || socket.Authenticated)
return new CallResult<bool>(true);
_log.Write(LogLevel.Debug, $"Attempting to authenticate {socket.SocketId}");
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
if (!result)
{
_log.Write(LogLevel.Warning, $"Socket {socket.SocketId} authentication failed");
if (socket.Connected)
await socket.CloseAsync().ConfigureAwait(false);
return await AuthenticateSocketAsync(socket).ConfigureAwait(false);
}
result.Error!.Message = "Authentication failed: " + result.Error.Message;
return new CallResult<bool>(result.Error);
/// <summary>
/// Authenticate a socket connection
/// </summary>
/// <param name="socket">Socket to authenticate</param>
/// <returns></returns>
public virtual async Task<CallResult<bool>> AuthenticateSocketAsync(SocketConnection socket)
{
if (AuthenticationProvider == null)
return new CallResult<bool>(new NoApiCredentialsError());
_logger.Log(LogLevel.Debug, $"[Sckt {socket.SocketId}] Attempting to authenticate");
var authRequest = GetAuthenticationRequest();
if (authRequest != null)
{
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
if (!result)
{
_logger.Log(LogLevel.Warning, $"[Sckt {socket.SocketId}] authentication failed");
if (socket.Connected)
await socket.CloseAsync().ConfigureAwait(false);
result.Error!.Message = "Authentication failed: " + result.Error.Message;
return new CallResult<bool>(result.Error)!;
}
}
_logger.Log(LogLevel.Debug, $"[Sckt {socket.SocketId}] authenticated");
socket.Authenticated = true;
return new CallResult<bool>(true);
}
/// <summary>
/// The socketConnection received data (the data JToken parameter). The implementation of this method should check if the received data is a response to the query that was send (the request parameter).
/// For example; A query is sent in a request message with an Id parameter with value 10. The socket receives data and calls this method to see if the data it received is an
/// anwser to any query that was done. The implementation of this method should check if the response.Id == request.Id to see if they match (assuming the api has some sort of Id tracking on messages,
/// if not some other method has be implemented to match the messages).
/// If the messages match, the callResult out parameter should be set with the deserialized data in the from of (T) and return true.
/// Should return the request which can be used to authenticate a socket connection
/// </summary>
/// <typeparam name="T">The type of response that is expected on the query</typeparam>
/// <param name="socketConnection">The socket connection</param>
/// <param name="request">The request that a response is awaited for</param>
/// <param name="data">The message received from the server</param>
/// <param name="callResult">The interpretation (null if message wasn't a response to the request)</param>
/// <returns>True if the message was a response to the query</returns>
protected internal abstract bool HandleQueryResponse<T>(SocketConnection socketConnection, object request, JToken data, [NotNullWhen(true)] out CallResult<T>? callResult);
/// <summary>
/// The socketConnection received data (the data JToken parameter). The implementation of this method should check if the received data is a response to the subscription request that was send (the request parameter).
/// For example; A subscribe request message is send with an Id parameter with value 10. The socket receives data and calls this method to see if the data it received is an
/// anwser to any subscription request that was done. The implementation of this method should check if the response.Id == request.Id to see if they match (assuming the api has some sort of Id tracking on messages,
/// if not some other method has be implemented to match the messages).
/// If the messages match, the callResult out parameter should be set with the deserialized data in the from of (T) and return true.
/// </summary>
/// <param name="socketConnection">The socket connection</param>
/// <param name="subscription">A subscription that waiting for a subscription response</param>
/// <param name="request">The request that the subscription sent</param>
/// <param name="data">The message received from the server</param>
/// <param name="callResult">The interpretation (null if message wasn't a response to the request)</param>
/// <returns>True if the message was a response to the subscription request</returns>
protected internal abstract bool HandleSubscriptionResponse(SocketConnection socketConnection, SocketSubscription subscription, object request, JToken data, out CallResult<object>? callResult);
/// <summary>
/// Needs to check if a received message matches a handler by request. After subscribing data message will come in. These data messages need to be matched to a specific connection
/// to pass the correct data to the correct handler. The implementation of this method should check if the message received matches the subscribe request that was sent.
/// </summary>
/// <param name="socketConnection">The socket connection the message was recieved on</param>
/// <param name="message">The received data</param>
/// <param name="request">The subscription request</param>
/// <returns>True if the message is for the subscription which sent the request</returns>
protected internal abstract bool MessageMatchesHandler(SocketConnection socketConnection, JToken message, object request);
/// <summary>
/// Needs to check if a received message matches a handler by identifier. Generally used by GenericHandlers. For example; a generic handler is registered which handles ping messages
/// from the server. This method should check if the message received is a ping message and the identifer is the identifier of the GenericHandler
/// </summary>
/// <param name="socketConnection">The socket connection the message was recieved on</param>
/// <param name="message">The received data</param>
/// <param name="identifier">The string identifier of the handler</param>
/// <returns>True if the message is for the handler which has the identifier</returns>
protected internal abstract bool MessageMatchesHandler(SocketConnection socketConnection, JToken message, string identifier);
/// <summary>
/// Needs to authenticate the socket so authenticated queries/subscriptions can be made on this socket connection
/// </summary>
/// <param name="socketConnection">The socket connection that should be authenticated</param>
/// <returns></returns>
protected internal abstract Task<CallResult<bool>> AuthenticateSocketAsync(SocketConnection socketConnection);
/// <summary>
/// Needs to unsubscribe a subscription, typically by sending an unsubscribe request. If multiple subscriptions per socket is not allowed this can just return since the socket will be closed anyway
/// </summary>
/// <param name="connection">The connection on which to unsubscribe</param>
/// <param name="subscriptionToUnsub">The subscription to unsubscribe</param>
/// <returns></returns>
protected internal abstract Task<bool> UnsubscribeAsync(SocketConnection connection, SocketSubscription subscriptionToUnsub);
protected internal virtual Query? GetAuthenticationRequest() => throw new NotImplementedException();
/// <summary>
/// Optional handler to interpolate data before sending it to the handlers
/// Adds a system subscription. Used for example to reply to ping requests
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
protected internal virtual JToken ProcessTokenData(JToken message)
/// <param name="systemSubscription">The subscription</param>
protected void AddSystemSubscription(SystemSubscription systemSubscription)
{
return message;
}
/// <summary>
/// Add a subscription to a connection
/// </summary>
/// <typeparam name="T">The type of data the subscription expects</typeparam>
/// <param name="request">The request of the subscription</param>
/// <param name="identifier">The identifier of the subscription (can be null if request param is used)</param>
/// <param name="userSubscription">Whether or not this is a user subscription (counts towards the max amount of handlers on a socket)</param>
/// <param name="connection">The socket connection the handler is on</param>
/// <param name="dataHandler">The handler of the data received</param>
/// <param name="authenticated">Whether the subscription needs authentication</param>
/// <returns></returns>
protected virtual SocketSubscription? AddSubscription<T>(object? request, string? identifier, bool userSubscription, SocketConnection connection, Action<DataEvent<T>> dataHandler, bool authenticated)
{
void InternalHandler(MessageEvent messageEvent)
{
if (typeof(T) == typeof(string))
{
var stringData = (T)Convert.ChangeType(messageEvent.JsonData.ToString(), typeof(T));
dataHandler(new DataEvent<T>(stringData, null, Options.OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp));
return;
}
var desResult = Deserialize<T>(messageEvent.JsonData);
if (!desResult)
{
_log.Write(LogLevel.Warning, $"Socket {connection.SocketId} Failed to deserialize data into type {typeof(T)}: {desResult.Error}");
return;
}
dataHandler(new DataEvent<T>(desResult.Data, null, Options.OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp));
}
var subscription = request == null
? SocketSubscription.CreateForIdentifier(NextId(), identifier!, userSubscription, authenticated, InternalHandler)
: SocketSubscription.CreateForRequest(NextId(), request, userSubscription, authenticated, InternalHandler);
if (!connection.AddSubscription(subscription))
return null;
return subscription;
}
/// <summary>
/// Adds a generic message handler. Used for example to reply to ping requests
/// </summary>
/// <param name="identifier">The name of the request handler. Needs to be unique</param>
/// <param name="action">The action to execute when receiving a message for this handler (checked by <see cref="MessageMatchesHandler(SocketConnection, Newtonsoft.Json.Linq.JToken,string)"/>)</param>
protected void AddGenericHandler(string identifier, Action<MessageEvent> action)
{
genericHandlers.Add(identifier, action);
var subscription = SocketSubscription.CreateForIdentifier(NextId(), identifier, false, false, action);
systemSubscriptions.Add(systemSubscription);
foreach (var connection in socketConnections.Values)
connection.AddSubscription(subscription);
connection.AddSubscription(systemSubscription);
}
/// <summary>
@@ -536,19 +418,19 @@ namespace CryptoExchange.Net
/// </summary>
/// <param name="connection"></param>
/// <returns></returns>
public virtual Task<Uri?> GetReconnectUriAsync(SocketConnection connection)
protected internal virtual Task<Uri?> GetReconnectUriAsync(SocketConnection connection)
{
return Task.FromResult<Uri?>(connection.ConnectionUri);
}
/// <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>
/// <param name="request">The original request</param>
/// <param name="subscription">The subscription</param>
/// <returns></returns>
public 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>
@@ -562,11 +444,11 @@ namespace CryptoExchange.Net
var socketResult = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
&& (s.Value.ApiClient.GetType() == GetType())
&& (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.SubscriptionCount).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;
if (result != null)
{
if (result.SubscriptionCount < Options.SocketSubscriptionsCombineTarget || (socketConnections.Count >= Options.MaxSocketConnections && socketConnections.All(s => s.Value.SubscriptionCount >= Options.SocketSubscriptionsCombineTarget)))
if (result.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
{
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
return new CallResult<SocketConnection>(result);
@@ -576,22 +458,23 @@ namespace CryptoExchange.Net
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
if (!connectionAddress)
{
_log.Write(LogLevel.Warning, $"Failed to determine connection url: " + connectionAddress.Error);
_logger.Log(LogLevel.Warning, $"Failed to determine connection url: " + connectionAddress.Error);
return connectionAddress.As<SocketConnection>(null);
}
if (connectionAddress.Data != address)
_log.Write(LogLevel.Debug, $"Connection address set to " + connectionAddress.Data);
_logger.Log(LogLevel.Debug, $"Connection address set to " + connectionAddress.Data);
// Create new socket
var socket = CreateSocket(connectionAddress.Data!);
var socketConnection = new SocketConnection(_log, this, socket, address);
var socketConnection = new SocketConnection(_logger, this, socket, address);
socketConnection.UnhandledMessage += HandleUnhandledMessage;
foreach (var kvp in genericHandlers)
{
var handler = SocketSubscription.CreateForIdentifier(NextId(), kvp.Key, false, false, kvp.Value);
socketConnection.AddSubscription(handler);
}
foreach (var ptg in PeriodicTaskRegistrations)
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, ptg.QueryDelegate, ptg.Callback);
foreach (var systemSubscription in systemSubscriptions)
socketConnection.AddSubscription(systemSubscription);
return new CallResult<SocketConnection>(socketConnection);
}
@@ -599,8 +482,8 @@ namespace CryptoExchange.Net
/// <summary>
/// Process an unhandled message
/// </summary>
/// <param name="token">The token that wasn't processed</param>
protected virtual void HandleUnhandledMessage(JToken token)
/// <param name="message">The message that wasn't processed</param>
protected virtual void HandleUnhandledMessage(IMessageAccessor message)
{
}
@@ -627,15 +510,13 @@ namespace CryptoExchange.Net
/// <param name="address">The address to connect to</param>
/// <returns></returns>
protected virtual WebSocketParameters GetWebSocketParameters(string address)
=> new(new Uri(address), Options.AutoReconnect)
=> new(new Uri(address), ClientOptions.AutoReconnect)
{
DataInterpreterBytes = dataInterpreterBytes,
DataInterpreterString = dataInterpreterString,
KeepAliveInterval = KeepAliveInterval,
ReconnectInterval = Options.ReconnectInterval,
RatelimitPerSecond = RateLimitPerSocketPerSecond,
ReconnectInterval = ClientOptions.ReconnectInterval,
RateLimiters = RateLimiters,
Proxy = ClientOptions.Proxy,
Timeout = Options.SocketNoDataTimeout
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout
};
/// <summary>
@@ -645,58 +526,11 @@ namespace CryptoExchange.Net
/// <returns></returns>
protected virtual IWebsocket CreateSocket(string address)
{
var socket = SocketFactory.CreateWebsocket(_log, GetWebSocketParameters(address));
_log.Write(LogLevel.Debug, $"Socket {socket.Id} new socket created for " + address);
var socket = SocketFactory.CreateWebsocket(_logger, GetWebSocketParameters(address));
_logger.Log(LogLevel.Debug, $"[Sckt {socket.Id}] created for " + address);
return socket;
}
/// <summary>
/// Periodically sends data over a socket connection
/// </summary>
/// <param name="identifier">Identifier for the periodic send</param>
/// <param name="interval">How often</param>
/// <param name="objGetter">Method returning the object to send</param>
public virtual void SendPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, object> objGetter)
{
if (objGetter == null)
throw new ArgumentNullException(nameof(objGetter));
periodicEvent = new AsyncResetEvent();
periodicTask = Task.Run(async () =>
{
while (!_disposing)
{
await periodicEvent.WaitAsync(interval).ConfigureAwait(false);
if (_disposing)
break;
foreach (var socketConnection in socketConnections.Values)
{
if (_disposing)
break;
if (!socketConnection.Connected)
continue;
var obj = objGetter(socketConnection);
if (obj == null)
continue;
_log.Write(LogLevel.Trace, $"Socket {socketConnection.SocketId} sending periodic {identifier}");
try
{
socketConnection.Send(obj);
}
catch (Exception ex)
{
_log.Write(LogLevel.Warning, $"Socket {socketConnection.SocketId} Periodic send {identifier} failed: " + ex.ToLogString());
}
}
}
});
}
/// <summary>
/// Unsubscribe an update subscription
/// </summary>
@@ -704,7 +538,7 @@ namespace CryptoExchange.Net
/// <returns></returns>
public virtual async Task<bool> UnsubscribeAsync(int subscriptionId)
{
SocketSubscription? subscription = null;
Subscription? subscription = null;
SocketConnection? connection = null;
foreach (var socket in socketConnections.Values.ToList())
{
@@ -719,7 +553,7 @@ namespace CryptoExchange.Net
if (subscription == null || connection == null)
return false;
_log.Write(LogLevel.Information, $"Socket {connection.SocketId} Unsubscribing subscription " + subscriptionId);
_logger.Log(LogLevel.Information, $"[Sckt {connection.SocketId}] unsubscribing subscription " + subscriptionId);
await connection.CloseAsync(subscription).ConfigureAwait(false);
return true;
}
@@ -734,7 +568,7 @@ namespace CryptoExchange.Net
if (subscription == null)
throw new ArgumentNullException(nameof(subscription));
_log.Write(LogLevel.Information, $"Socket {subscription.SocketId} Unsubscribing subscription " + subscription.Id);
_logger.Log(LogLevel.Information, $"[Sckt {subscription.SocketId}] Unsubscribing subscription " + subscription.Id);
await subscription.CloseAsync().ConfigureAwait(false);
}
@@ -744,12 +578,16 @@ namespace CryptoExchange.Net
/// <returns></returns>
public virtual async Task UnsubscribeAllAsync()
{
_log.Write(LogLevel.Information, $"Unsubscribing all {socketConnections.Sum(s => s.Value.SubscriptionCount)} subscriptions");
var sum = socketConnections.Sum(s => s.Value.UserSubscriptionCount);
if (sum == 0)
return;
_logger.Log(LogLevel.Information, $"Unsubscribing all {socketConnections.Sum(s => s.Value.UserSubscriptionCount)} subscriptions");
var tasks = new List<Task>();
{
var socketList = socketConnections.Values;
foreach (var sub in socketList)
tasks.Add(sub.CloseAsync());
tasks.Add(sub.CloseAsync());
}
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
@@ -761,7 +599,7 @@ namespace CryptoExchange.Net
/// <returns></returns>
public virtual async Task ReconnectAsync()
{
_log.Write(LogLevel.Information, $"Reconnecting all {socketConnections.Count} connections");
_logger.Log(LogLevel.Information, $"Reconnecting all {socketConnections.Count} connections");
var tasks = new List<Task>();
{
var socketList = socketConnections.Values;
@@ -778,12 +616,26 @@ namespace CryptoExchange.Net
public string GetSubscriptionsState()
{
var sb = new StringBuilder();
sb.AppendLine($"{socketConnections.Count} connections, {CurrentSubscriptions} subscriptions, kbps: {IncomingKbps}");
sb.AppendLine($"{GetType().Name}");
sb.AppendLine($" Connections: {socketConnections.Count}");
sb.AppendLine($" Subscriptions: {CurrentSubscriptions}");
sb.AppendLine($" Download speed: {IncomingKbps} kbps");
foreach (var connection in socketConnections)
{
sb.AppendLine($" Connection {connection.Key}: {connection.Value.SubscriptionCount} subscriptions, status: {connection.Value.Status}, authenticated: {connection.Value.Authenticated}, kbps: {connection.Value.IncomingKbps}");
sb.AppendLine($" Id: {connection.Key}");
sb.AppendLine($" Address: {connection.Value.ConnectionUri}");
sb.AppendLine($" Subscriptions: {connection.Value.UserSubscriptionCount}");
sb.AppendLine($" Status: {connection.Value.Status}");
sb.AppendLine($" Authenticated: {connection.Value.Authenticated}");
sb.AppendLine($" Download speed: {connection.Value.IncomingKbps} kbps");
sb.AppendLine($" Subscriptions:");
foreach (var subscription in connection.Value.Subscriptions)
sb.AppendLine($" Subscription {subscription.Id}, authenticated: {subscription.Authenticated}, confirmed: {subscription.Confirmed}");
{
sb.AppendLine($" Id: {subscription.Id}");
sb.AppendLine($" Confirmed: {subscription.Confirmed}");
sb.AppendLine($" Invocations: {subscription.TotalInvocations}");
sb.AppendLine($" Identifiers: [{string.Join(", ", subscription.ListenerIdentifiers)}]");
}
}
return sb.ToString();
}
@@ -794,15 +646,28 @@ namespace CryptoExchange.Net
public override void Dispose()
{
_disposing = true;
periodicEvent?.Set();
periodicEvent?.Dispose();
if (socketConnections.Sum(s => s.Value.SubscriptionCount) > 0)
if (socketConnections.Sum(s => s.Value.UserSubscriptionCount) > 0)
{
_log.Write(LogLevel.Debug, "Disposing socket client, closing all subscriptions");
_logger.Log(LogLevel.Debug, "Disposing socket client, closing all subscriptions");
_ = UnsubscribeAllAsync();
}
semaphoreSlim?.Dispose();
base.Dispose();
}
/// <summary>
/// Get the listener identifier for the message
/// </summary>
/// <param name="messageAccessor"></param>
/// <returns></returns>
public abstract string? GetListenerIdentifier(IMessageAccessor messageAccessor);
/// <summary>
/// Preprocess a stream message
/// </summary>
/// <param name="type"></param>
/// <param name="stream"></param>
/// <returns></returns>
public virtual Stream PreprocessStreamMessage(WebSocketMessageType type, Stream stream) => stream;
}
}
+1 -5
View File
@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.CommonObjects
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Order type
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.CommonObjects
{
+1 -5
View File
@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.CommonObjects
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Id of an order
+1 -3
View File
@@ -1,6 +1,4 @@
using System;
namespace CryptoExchange.Net.CommonObjects
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Ticker data
@@ -16,8 +16,8 @@ namespace CryptoExchange.Net.Converters
/// </summary>
public class ArrayConverter : JsonConverter
{
private static readonly ConcurrentDictionary<(MemberInfo, Type), Attribute> attributeByMemberInfoAndTypeCache = new ConcurrentDictionary<(MemberInfo, Type), Attribute>();
private static readonly ConcurrentDictionary<(Type, Type), Attribute> attributeByTypeAndTypeCache = new ConcurrentDictionary<(Type, Type), Attribute>();
private static readonly ConcurrentDictionary<(MemberInfo, Type), Attribute> _attributeByMemberInfoAndTypeCache = new ConcurrentDictionary<(MemberInfo, Type), Attribute>();
private static readonly ConcurrentDictionary<(Type, Type), Attribute> _attributeByTypeAndTypeCache = new ConcurrentDictionary<(Type, Type), Attribute>();
/// <inheritdoc />
public override bool CanConvert(Type objectType)
@@ -100,18 +100,30 @@ namespace CryptoExchange.Net.Converters
}
if (value != null && property.PropertyType.IsInstanceOfType(value))
{
property.SetValue(result, value);
}
else
{
if (value is JToken token)
{
if (token.Type == JTokenType.Null)
value = null;
if ((property.PropertyType == typeof(decimal)
if (token.Type == JTokenType.Float)
value = token.Value<decimal>();
}
if (value is decimal)
{
property.SetValue(result, value);
}
else if ((property.PropertyType == typeof(decimal)
|| property.PropertyType == typeof(decimal?))
&& (value != null && value.ToString().IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0))
{
if (decimal.TryParse(value.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
var v = value.ToString();
if (decimal.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
property.SetValue(result, dec);
}
else
@@ -175,10 +187,10 @@ namespace CryptoExchange.Net.Converters
}
private static T? GetCustomAttribute<T>(MemberInfo memberInfo) where T : Attribute =>
(T?)attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T)));
(T?)_attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T)));
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>
@@ -16,7 +16,7 @@ namespace CryptoExchange.Net.Converters
/// The enum->string mapping
/// </summary>
protected abstract List<KeyValuePair<T, string>> Mapping { get; }
private readonly bool quotes;
private readonly bool _quotes;
/// <summary>
/// ctor
@@ -24,14 +24,14 @@ namespace CryptoExchange.Net.Converters
/// <param name="useQuotes"></param>
protected BaseConverter(bool useQuotes)
{
quotes = useQuotes;
_quotes = useQuotes;
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
var stringValue = value == null? null: GetValue((T) value);
if (quotes)
if (_quotes)
writer.WriteValue(stringValue);
else
writer.WriteRawValue(stringValue);
@@ -0,0 +1,80 @@
using System;
using Newtonsoft.Json;
namespace CryptoExchange.Net.Converters
{
/// <summary>
/// Boolean converter with support for "0"/"1" (strings)
/// </summary>
public class BoolConverter : JsonConverter
{
/// <summary>
/// Determines whether this instance can convert the specified object type.
/// </summary>
/// <param name="objectType">Type of the object.</param>
/// <returns>
/// <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
/// </returns>
public override bool CanConvert(Type objectType)
{
if (Nullable.GetUnderlyingType(objectType) != null)
return Nullable.GetUnderlyingType(objectType) == typeof(bool);
return objectType == typeof(bool);
}
/// <summary>
/// Reads the JSON representation of the object.
/// </summary>
/// <param name="reader">The <see cref="T:Newtonsoft.Json.JsonReader"/> to read from.</param>
/// <param name="objectType">Type of the object.</param>
/// <param name="existingValue">The existing value of object being read.</param>
/// <param name="serializer">The calling serializer.</param>
/// <returns>
/// The object value.
/// </returns>
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
var value = reader.Value?.ToString().ToLower().Trim();
if (value == null || value == "")
{
if (Nullable.GetUnderlyingType(objectType) != null)
return null;
return false;
}
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;
}
// If we reach here, we're pretty much going to throw an error so let's let Json.NET throw it's pretty-fied error message.
return new JsonSerializer().Deserialize(reader, objectType);
}
/// <summary>
/// Specifies that this converter will not participate in writing results.
/// </summary>
public override bool CanWrite { get { return false; } }
/// <summary>
/// Writes the JSON representation of the object.
/// </summary>
/// <param name="writer">The <see cref="T:Newtonsoft.Json.JsonWriter"/> to write to.</param><param name="value">The value.</param><param name="serializer">The calling serializer.</param>
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
}
}
}
@@ -12,9 +12,9 @@ namespace CryptoExchange.Net.Converters
public class DateTimeConverter: JsonConverter
{
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private const long ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
private const decimal ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000;
private const decimal ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000;
private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
/// <inheritdoc />
public override bool CanConvert(Type objectType)
@@ -59,8 +59,12 @@ namespace CryptoExchange.Net.Converters
if (string.IsNullOrWhiteSpace(stringValue))
return null;
if (string.IsNullOrWhiteSpace(stringValue) || stringValue == "0" || stringValue == "-1")
if (string.IsNullOrWhiteSpace(stringValue)
|| stringValue == "-1"
|| (double.TryParse(stringValue, out var doubleVal) && doubleVal == 0))
{
return objectType == typeof(DateTime) ? default(DateTime) : null;
}
if (stringValue.Length == 8)
{
@@ -134,7 +138,7 @@ namespace CryptoExchange.Net.Converters
/// </summary>
/// <param name="seconds"></param>
/// <returns></returns>
public static DateTime ConvertFromSeconds(double seconds) => _epoch.AddTicks((long)Math.Round(seconds * ticksPerSecond));
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>
@@ -146,13 +150,13 @@ namespace CryptoExchange.Net.Converters
/// </summary>
/// <param name="microseconds"></param>
/// <returns></returns>
public static DateTime ConvertFromMicroseconds(long microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * ticksPerMicrosecond));
public static DateTime ConvertFromMicroseconds(long 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(long nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * ticksPerNanosecond));
public static DateTime ConvertFromNanoseconds(long nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
/// <summary>
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
/// </summary>
@@ -173,14 +177,14 @@ namespace CryptoExchange.Net.Converters
/// <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);
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);
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
/// <inheritdoc />
@@ -0,0 +1,27 @@
using Newtonsoft.Json;
using System;
using System.Globalization;
namespace Kraken.Net.Converters
{
/// <summary>
/// Converter for serializing decimal values as string
/// </summary>
public class DecimalStringWriterConverter : JsonConverter
{
/// <inheritdoc />
public override bool CanRead => false;
/// <inheritdoc />
public override bool CanConvert(Type objectType) => objectType == typeof(decimal) || objectType == typeof(decimal?);
/// <inheritdoc />
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => writer.WriteValue(((decimal?)value)?.ToString(CultureInfo.InvariantCulture) ?? null);
}
}
+46 -8
View File
@@ -14,12 +14,29 @@ namespace CryptoExchange.Net.Converters
/// </summary>
public class EnumConverter : JsonConverter
{
private bool _warnOnMissingEntry = true;
private bool _writeAsInt;
/// <summary>
/// </summary>
public EnumConverter() { }
/// <summary>
/// </summary>
/// <param name="writeAsInt"></param>
/// <param name="warnOnMissingEntry"></param>
public EnumConverter(bool writeAsInt, bool warnOnMissingEntry)
{
_writeAsInt = writeAsInt;
_warnOnMissingEntry = warnOnMissingEntry;
}
private static readonly ConcurrentDictionary<Type, List<KeyValuePair<object, string>>> _mapping = new();
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
return objectType.IsEnum;
return objectType.IsEnum || Nullable.GetUnderlyingType(objectType)?.IsEnum == true;
}
/// <inheritdoc />
@@ -30,7 +47,7 @@ namespace CryptoExchange.Net.Converters
mapping = AddMapping(enumType);
var stringValue = reader.Value?.ToString();
if (stringValue == null)
if (stringValue == null || stringValue == "")
{
// Received null value
var emptyResult = GetDefaultValue(objectType, enumType);
@@ -51,8 +68,12 @@ namespace CryptoExchange.Net.Converters
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.
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {enumType.Name}, Value: {reader.Value}, Known values: {string.Join(", ", mapping.Select(m => m.Value))}. If you think {reader.Value} should added please open an issue on the Github repo");
if (_warnOnMissingEntry)
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {enumType.Name}, Value: {reader.Value}, Known values: {string.Join(", ", mapping.Select(m => m.Value))}. If you think {reader.Value} should added please open an issue on the Github repo");
}
return defaultValue;
}
@@ -117,22 +138,39 @@ namespace CryptoExchange.Net.Converters
/// <param name="enumValue"></param>
/// <returns></returns>
[return: NotNullIfNotNull("enumValue")]
public static string? GetString<T>(T enumValue)
public static string? GetString<T>(T enumValue) => GetString(typeof(T), enumValue);
[return: NotNullIfNotNull("enumValue")]
private static string? GetString(Type objectType, object? enumValue)
{
var objectType = typeof(T);
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());
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
var stringValue = GetString(value);
writer.WriteValue(stringValue);
if (value == null)
{
writer.WriteNull();
}
else
{
if (!_writeAsInt)
{
var stringValue = GetString(value.GetType(), value);
writer.WriteValue(stringValue);
}
else
{
writer.WriteValue((int)value);
}
}
}
}
}
@@ -0,0 +1,35 @@
using Newtonsoft.Json;
using System.Globalization;
namespace CryptoExchange.Net.Converters
{
/// <summary>
/// Serializer options
/// </summary>
public static class SerializerOptions
{
/// <summary>
/// Json serializer settings which includes the EnumConverter, DateTimeConverter and BoolConverter
/// </summary>
public static JsonSerializerSettings WithConverters => new JsonSerializerSettings
{
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
Culture = CultureInfo.InvariantCulture,
Converters =
{
new EnumConverter(),
new DateTimeConverter(),
new BoolConverter()
}
};
/// <summary>
/// Default json serializer settings
/// </summary>
public static JsonSerializerSettings Default => new JsonSerializerSettings
{
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
Culture = CultureInfo.InvariantCulture
};
}
}
+15 -8
View File
@@ -6,20 +6,26 @@
<PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors>
<Description>A base package for implementing cryptocurrency API's</Description>
<PackageVersion>5.4.2</PackageVersion>
<AssemblyVersion>5.4.2</AssemblyVersion>
<FileVersion>5.4.2</FileVersion>
<PackageVersion>7.0.0-beta2</PackageVersion>
<AssemblyVersion>7.0.0-beta2</AssemblyVersion>
<FileVersion>7.0.0-beta2</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
<NeutralLanguage>en</NeutralLanguage>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>icon.png</PackageIcon>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageReleaseNotes>5.4.2 - Reverted socket changes as it seems to cause reconnect to hang</PackageReleaseNotes>
<PackageReleaseNotes>7.0.0-beta2 - Updated RevitalizeRequestAsync signature, Removed duplicate logging</PackageReleaseNotes>
<Nullable>enable</Nullable>
<LangVersion>9.0</LangVersion>
<LangVersion>10.0</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<None Include="Icon\icon.png" Pack="true" PackagePath="\" />
<None Include="..\README.md" Pack="true" PackagePath="\" />
</ItemGroup>
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
@@ -41,14 +47,15 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="6.0.0">
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.32" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="[3.1.0,)" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="[3.1.0,)" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.32" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.32" />
</ItemGroup>
</Project>
+75
View File
@@ -1,5 +1,6 @@
using CryptoExchange.Net.Objects;
using System;
using System.Security.Cryptography;
namespace CryptoExchange.Net
{
@@ -8,6 +9,17 @@ namespace CryptoExchange.Net
/// </summary>
public static class ExchangeHelpers
{
private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789";
/// <summary>
/// The last used id, use NextId() to get the next id and up this
/// </summary>
private static int _lastId;
/// <summary>
/// Lock for id generating
/// </summary>
private static object _idLock = new();
/// <summary>
/// Clamp a value between a min and max
/// </summary>
@@ -43,7 +55,9 @@ namespace CryptoExchange.Net
var offset = value % step.Value;
if(roundingType == RoundingType.Down)
{
value -= offset;
}
else
{
if (offset < step / 2)
@@ -116,5 +130,66 @@ namespace CryptoExchange.Net
{
return value / 1.000000000000000000000000000000000m;
}
/// <summary>
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique
/// </summary>
/// <returns></returns>
public static int NextId()
{
lock (_idLock)
{
_lastId += 1;
return _lastId;
}
}
/// <summary>
/// Return the last unique id that was generated
/// </summary>
/// <returns></returns>
public static int LastId()
{
lock (_idLock)
return _lastId;
}
/// <summary>
/// Generate a random string of specified length
/// </summary>
/// <param name="length">Length of the random string</param>
/// <returns></returns>
public static string RandomString(int length)
{
var randomChars = new char[length];
#if NETSTANDARD2_1_OR_GREATER
for (int i = 0; i < length; i++)
randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)];
#else
var random = new Random();
for (int i = 0; i < length; i++)
randomChars[i] = _allowedRandomChars[random.Next(0, _allowedRandomChars.Length)];
#endif
return new string(randomChars);
}
/// <summary>
/// Generate a random string of specified length
/// </summary>
/// <param name="source">The initial string</param>
/// <param name="totalLength">Total length of the resulting string</param>
/// <returns></returns>
public static string AppendRandomString(string source, int totalLength)
{
if (totalLength < source.Length)
throw new ArgumentException("Total length smaller than source string length", nameof(totalLength));
if (totalLength == source.Length)
return source;
return source + RandomString(totalLength - source.Length);
}
}
}
+23 -34
View File
@@ -6,7 +6,6 @@ using System.Runtime.InteropServices;
using System.Security;
using System.Text;
using System.Web;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
@@ -90,31 +89,6 @@ namespace CryptoExchange.Net
parameters.Add(key, JsonConvert.SerializeObject(value, converter));
}
/// <summary>
/// Add an optional parameter. Not added if value is null
/// </summary>
/// <param name="parameters"></param>
/// <param name="key"></param>
/// <param name="value"></param>
public static void AddOptionalParameter(this Dictionary<string, string> parameters, string key, string? value)
{
if (value != null)
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, string> parameters, string key, string? value, JsonConverter converter)
{
if (value != null)
parameters.Add(key, JsonConvert.SerializeObject(value, converter));
}
/// <summary>
/// Create a query string of the specified parameters
/// </summary>
@@ -129,7 +103,9 @@ namespace CryptoExchange.Net
foreach (var arrayEntry in arraysParameters)
{
if (serializationType == ArrayParametersSerialization.Array)
{
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={v}"))}&";
}
else
{
var array = (Array)arrayEntry.Value;
@@ -160,7 +136,9 @@ namespace CryptoExchange.Net
formData.Add(kvp.Key, value.ToString());
}
else
{
formData.Add(kvp.Key, kvp.Value.ToString());
}
}
return formData.ToString();
}
@@ -224,7 +202,11 @@ namespace CryptoExchange.Net
if (b1 != b2) return false;
}
}
else return false;
else
{
return false;
}
return true;
}
finally
@@ -252,9 +234,9 @@ namespace CryptoExchange.Net
/// String to JToken
/// </summary>
/// <param name="stringData"></param>
/// <param name="log"></param>
/// <param name="logger"></param>
/// <returns></returns>
public static JToken? ToJToken(this string stringData, Log? log = null)
public static JToken? ToJToken(this string stringData, ILogger? logger = null)
{
if (string.IsNullOrEmpty(stringData))
return null;
@@ -266,15 +248,15 @@ namespace CryptoExchange.Net
catch (JsonReaderException jre)
{
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}. Data: {stringData}";
log?.Write(LogLevel.Error, info);
if (log == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
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}";
log?.Write(LogLevel.Error, info);
if (log == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
logger?.Log(LogLevel.Error, info);
if (logger == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
return null;
}
}
@@ -288,8 +270,10 @@ namespace CryptoExchange.Net
public static void ValidateIntValues(this int value, string argumentName, params int[] allowedValues)
{
if (!allowedValues.Contains(value))
{
throw new ArgumentException(
$"{value} not allowed for parameter {argumentName}, allowed values: {string.Join(", ", allowedValues)}", argumentName);
}
}
/// <summary>
@@ -302,8 +286,10 @@ namespace CryptoExchange.Net
public static void ValidateIntBetween(this int value, string argumentName, int minValue, int maxValue)
{
if (value < minValue || value > maxValue)
{
throw new ArgumentException(
$"{value} not allowed for parameter {argumentName}, min: {minValue}, max: {maxValue}", argumentName);
}
}
/// <summary>
@@ -437,7 +423,9 @@ namespace CryptoExchange.Net
httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString());
}
else
{
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
}
}
uriBuilder.Query = httpValueCollection.ToString();
return uriBuilder.Uri;
@@ -466,13 +454,14 @@ namespace CryptoExchange.Net
httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString());
}
else
{
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
}
}
uriBuilder.Query = httpValueCollection.ToString();
return uriBuilder.Uri;
}
/// <summary>
/// Add parameter to URI
/// </summary>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -2,7 +2,6 @@
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.CommonObjects;
using CryptoExchange.Net.Interfaces.CommonClients;
using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net.Interfaces.CommonClients
@@ -1,5 +1,4 @@
using CryptoExchange.Net.CommonObjects;
using CryptoExchange.Net.Interfaces.CommonClients;
using CryptoExchange.Net.Objects;
using System.Threading;
using System.Threading.Tasks;
@@ -7,6 +7,11 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
public interface IBaseApiClient
{
/// <summary>
/// Base address
/// </summary>
string BaseAddress { get; }
/// <summary>
/// Set the API credentials for this API client
/// </summary>
@@ -0,0 +1,34 @@
using CryptoExchange.Net.Interfaces.CommonClients;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Client for accessing REST API's for different exchanges
/// </summary>
public interface ICryptoRestClient
{
/// <summary>
/// Get a list of all registered common ISpotClient types
/// </summary>
/// <returns></returns>
IEnumerable<ISpotClient> GetSpotClients();
/// <summary>
/// Get an ISpotClient implementation by exchange name
/// </summary>
/// <param name="exchangeName"></param>
/// <returns></returns>
ISpotClient? SpotClient(string exchangeName);
/// <summary>
/// Try get
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
T TryGet<T>(Func<T> createFunc);
}
}
@@ -0,0 +1,21 @@
using CryptoExchange.Net.Interfaces.CommonClients;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Client for accessing Websocket API's for different exchanges
/// </summary>
public interface ICryptoSocketClient
{
/// <summary>
/// Try get a client by type for the service collection
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
T TryGet<T>(Func<T> createFunc);
}
}
@@ -0,0 +1,45 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Message processor
/// </summary>
public interface IMessageProcessor
{
/// <summary>
/// Id of the processor
/// </summary>
public int Id { get; }
/// <summary>
/// The identifiers for this processor
/// </summary>
public HashSet<string> ListenerIdentifiers { get; }
/// <summary>
/// Handle a message
/// </summary>
/// <param name="connection"></param>
/// <param name="message"></param>
/// <returns></returns>
Task<CallResult> HandleAsync(SocketConnection connection, DataEvent<object> message);
/// <summary>
/// Get the type the message should be deserialized to
/// </summary>
/// <param name="messageAccessor"></param>
/// <returns></returns>
Type? GetMessageType(IMessageAccessor messageAccessor);
/// <summary>
/// Deserialize a message int oobject of type
/// </summary>
/// <param name="accessor"></param>
/// <param name="type"></param>
/// <returns></returns>
object Deserialize(IMessageAccessor accessor, Type type);
}
}
@@ -1,5 +1,5 @@
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
using System.Net.Http;
using System.Security;
using System.Threading;
@@ -24,6 +24,6 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="requestWeight">The weight of the request</param>
/// <param name="ct">Cancellation token to cancel waiting</param>
/// <returns>The time in milliseconds spend waiting</returns>
Task<CallResult<int>> LimitRequestAsync(Log log, string endpoint, HttpMethod method, bool signed, SecureString? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct);
Task<CallResult<int>> LimitRequestAsync(ILogger log, string endpoint, HttpMethod method, bool signed, SecureString? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct);
}
}
@@ -22,8 +22,8 @@ namespace CryptoExchange.Net.Interfaces
/// Configure the requests created by this factory
/// </summary>
/// <param name="requestTimeout">Request timeout to use</param>
/// <param name="proxy">Proxy settings to use</param>
/// <param name="httpClient">Optional shared http client instance</param>
void Configure(TimeSpan requestTimeout, ApiProxy? proxy, HttpClient? httpClient=null);
/// <param name="proxy">Optional proxy to use when no http client is provided</param>
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient=null);
}
}
@@ -20,6 +20,11 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
bool IsSuccessStatusCode { get; }
/// <summary>
/// The length of the response in bytes
/// </summary>
long? ContentLength { get; }
/// <summary>
/// The response headers
/// </summary>
@@ -1,7 +1,4 @@
using CryptoExchange.Net.Objects;
using System;
namespace CryptoExchange.Net.Interfaces
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Base rest API client
@@ -17,17 +14,5 @@ namespace CryptoExchange.Net.Interfaces
/// Total amount of requests made with this API client
/// </summary>
int TotalRequestsMade { get; set; }
/// <summary>
/// Get time offset for an API client. Return null if time syncing shouldnt/cant be done
/// </summary>
/// <returns></returns>
TimeSpan? GetTimeOffset();
/// <summary>
/// Get time sync info for an API client. Return null if time syncing shouldnt/cant be done
/// </summary>
/// <returns></returns>
TimeSyncInfo? GetTimeSyncInfo();
}
}
+2 -3
View File
@@ -1,6 +1,5 @@
using System;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
namespace CryptoExchange.Net.Interfaces
{
@@ -12,7 +11,7 @@ namespace CryptoExchange.Net.Interfaces
/// <summary>
/// The options provided for this client
/// </summary>
ClientOptions ClientOptions { get; }
ExchangeOptions ClientOptions { get; }
/// <summary>
/// The total amount of requests made with this client
@@ -1,6 +1,5 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using System;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces
@@ -23,21 +22,17 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
double IncomingKbps { get; }
/// <summary>
/// Client options
/// </summary>
SocketApiClientOptions Options { get; }
/// <summary>
/// The factory for creating sockets. Used for unit testing
/// </summary>
IWebsocketFactory SocketFactory { get; set; }
/// <summary>
/// Get the url to reconnect to after losing a connection
/// Current client options
/// </summary>
/// <param name="connection"></param>
/// <returns></returns>
Task<Uri?> GetReconnectUriAsync(SocketConnection connection);
SocketExchangeOptions ClientOptions { get; }
/// <summary>
/// Current API options
/// </summary>
SocketApiOptions ApiOptions { get; }
/// <summary>
/// Log the current state of connections and subscriptions
/// </summary>
@@ -48,19 +43,6 @@ namespace CryptoExchange.Net.Interfaces
/// <returns></returns>
Task ReconnectAsync();
/// <summary>
/// Update the original request to send when the connection is restored after disconnecting. Can be used to update an authentication token for example.
/// </summary>
/// <param name="request">The original request</param>
/// <returns></returns>
Task<CallResult<object>> RevitalizeRequestAsync(object request);
/// <summary>
/// Periodically sends data over a socket connection
/// </summary>
/// <param name="identifier">Identifier for the periodic send</param>
/// <param name="interval">How often</param>
/// <param name="objGetter">Method returning the object to send</param>
void SendPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, object> objGetter);
/// <summary>
/// Unsubscribe all subscriptions
/// </summary>
/// <returns></returns>
@@ -1,8 +1,7 @@
using System;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
namespace CryptoExchange.Net.Interfaces
{
@@ -14,7 +13,7 @@ namespace CryptoExchange.Net.Interfaces
/// <summary>
/// The options provided for this client
/// </summary>
ClientOptions ClientOptions { get; }
ExchangeOptions ClientOptions { get; }
/// <summary>
/// Incoming kilobytes per second of data
+17 -13
View File
@@ -1,41 +1,43 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using System;
using System.Security.Authentication;
using System.Text;
using System;
using System.IO;
using System.Net.WebSockets;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Webscoket connection interface
/// Websocket connection interface
/// </summary>
public interface IWebsocket: IDisposable
{
/// <summary>
/// Websocket closed event
/// </summary>
event Action OnClose;
event Func<Task> OnClose;
/// <summary>
/// Websocket message received event
/// </summary>
event Action<string> OnMessage;
event Func<WebSocketMessageType, Stream, Task> OnStreamMessage;
/// <summary>
/// Websocket sent event, RequestId as parameter
/// </summary>
event Func<int, Task> OnRequestSent;
/// <summary>
/// Websocket error event
/// </summary>
event Action<Exception> OnError;
event Func<Exception, Task> OnError;
/// <summary>
/// Websocket opened event
/// </summary>
event Action OnOpen;
event Func<Task> OnOpen;
/// <summary>
/// Websocket has lost connection to the server and is attempting to reconnect
/// </summary>
event Action OnReconnecting;
event Func<Task> OnReconnecting;
/// <summary>
/// Websocket has reconnected to the server
/// </summary>
event Action OnReconnected;
event Func<Task> OnReconnected;
/// <summary>
/// Get reconntion url
/// </summary>
@@ -69,8 +71,10 @@ namespace CryptoExchange.Net.Interfaces
/// <summary>
/// Send data
/// </summary>
/// <param name="id"></param>
/// <param name="data"></param>
void Send(string data);
/// <param name="weight"></param>
void Send(int id, string data, int weight);
/// <summary>
/// Reconnect the socket
/// </summary>
@@ -1,5 +1,5 @@
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Interfaces
{
@@ -11,9 +11,9 @@ namespace CryptoExchange.Net.Interfaces
/// <summary>
/// Create a websocket for an url
/// </summary>
/// <param name="log">The logger</param>
/// <param name="logger">The logger</param>
/// <param name="parameters">The parameters to use for the connection</param>
/// <returns></returns>
IWebsocket CreateWebsocket(Log log, WebSocketParameters parameters);
IWebsocket CreateWebsocket(ILogger logger, WebSocketParameters parameters);
}
}
@@ -1,24 +0,0 @@
using Microsoft.Extensions.Logging;
using System;
namespace CryptoExchange.Net.Logging
{
/// <summary>
/// ILogger implementation for logging to the console
/// </summary>
public class ConsoleLogger : ILogger
{
/// <inheritdoc />
public IDisposable BeginScope<TState>(TState state) => null!;
/// <inheritdoc />
public bool IsEnabled(LogLevel logLevel) => true;
/// <inheritdoc />
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {formatter(state, exception)}";
Console.WriteLine(logMessage);
}
}
}
-25
View File
@@ -1,25 +0,0 @@
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
namespace CryptoExchange.Net.Logging
{
/// <summary>
/// Default log writer, uses Trace.WriteLine
/// </summary>
public class DebugLogger: ILogger
{
/// <inheritdoc />
public IDisposable BeginScope<TState>(TState state) => null!;
/// <inheritdoc />
public bool IsEnabled(LogLevel logLevel) => true;
/// <inheritdoc />
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {formatter(state, exception)}";
Trace.WriteLine(logMessage);
}
}
}
-79
View File
@@ -1,79 +0,0 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace CryptoExchange.Net.Logging
{
/// <summary>
/// Log implementation
/// </summary>
public class Log
{
/// <summary>
/// List of ILogger implementations to forward the message to
/// </summary>
private List<ILogger> writers;
/// <summary>
/// The verbosity of the logging, anything more verbose will not be forwarded to the writers
/// </summary>
public LogLevel? Level { get; set; } = LogLevel.Information;
/// <summary>
/// Client name
/// </summary>
public string ClientName { get; set; }
private readonly object _lock = new object();
/// <summary>
/// ctor
/// </summary>
/// <param name="clientName">The name of the client the logging is used in</param>
public Log(string clientName)
{
ClientName = clientName;
writers = new List<ILogger>();
}
/// <summary>
/// Set the writers
/// </summary>
/// <param name="textWriters"></param>
public void UpdateWriters(List<ILogger> textWriters)
{
lock (_lock)
writers = textWriters;
}
/// <summary>
/// Write a log entry
/// </summary>
/// <param name="logLevel">The verbosity of the message</param>
/// <param name="message">The message to log</param>
public void Write(LogLevel logLevel, string message)
{
if (Level != null && (int)logLevel < (int)Level)
return;
var logMessage = $"{ClientName,-10} | {message}";
lock (_lock)
{
foreach (var writer in writers)
{
try
{
writer.Log(logLevel, logMessage);
}
catch (Exception e)
{
// Can't write to the logging so where else to output..
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Failed to write log to writer {writer.GetType()}: " + e.ToLogString());
}
}
}
}
}
}
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -12,7 +13,7 @@ namespace CryptoExchange.Net.Objects
public class AsyncResetEvent : IDisposable
{
private static readonly Task<bool> _completed = Task.FromResult(true);
private readonly Queue<TaskCompletionSource<bool>> _waits = new Queue<TaskCompletionSource<bool>>();
private Queue<TaskCompletionSource<bool>> _waits = new Queue<TaskCompletionSource<bool>>();
private bool _signaled;
private readonly bool _reset;
@@ -49,7 +50,13 @@ namespace CryptoExchange.Net.Objects
var cancellationSource = new CancellationTokenSource(timeout.Value);
var registration = cancellationSource.Token.Register(() =>
{
tcs.TrySetResult(false);
lock (_waits)
{
tcs.TrySetResult(false);
// Not the cleanest but it works
_waits = new Queue<TaskCompletionSource<bool>>(_waits.Where(i => i != tcs));
}
}, useSynchronizationContext: false);
}
+103 -23
View File
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Http;
using System.Text;
namespace CryptoExchange.Net.Objects
{
@@ -38,6 +39,12 @@ namespace CryptoExchange.Net.Objects
{
return obj?.Success == true;
}
/// <inheritdoc />
public override string ToString()
{
return Success ? $"Success" : $"Error: {Error}";
}
}
/// <summary>
@@ -63,7 +70,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="originalData"></param>
/// <param name="error"></param>
#pragma warning disable 8618
protected CallResult([AllowNull]T data, string? originalData, Error? error): base(error)
public CallResult([AllowNull]T data, string? originalData, Error? error): base(error)
#pragma warning restore 8618
{
OriginalData = originalData;
@@ -84,6 +91,13 @@ namespace CryptoExchange.Net.Objects
/// <param name="error">The erro rto return</param>
public CallResult(Error error) : this(default, null, error) { }
/// <summary>
/// Create a new error result
/// </summary>
/// <param name="error">The error to return</param>
/// <param name="originalData">The original response data</param>
public CallResult(Error error, string? originalData) : this(default, originalData, error) { }
/// <summary>
/// Overwrite bool check so we can use if(callResult) instead of if(callResult.Success)
/// </summary>
@@ -128,6 +142,24 @@ namespace CryptoExchange.Net.Objects
return new CallResult<K>(data, OriginalData, Error);
}
/// <summary>
/// Copy as a dataless result
/// </summary>
/// <returns></returns>
public CallResult AsDataless()
{
return new CallResult(null);
}
/// <summary>
/// Copy as a dataless result
/// </summary>
/// <returns></returns>
public CallResult AsDatalessError(Error error)
{
return new CallResult(error);
}
/// <summary>
/// Copy the WebCallResult to a new data type
/// </summary>
@@ -138,6 +170,12 @@ namespace CryptoExchange.Net.Objects
{
return new CallResult<K>(default, OriginalData, error);
}
/// <inheritdoc />
public override string ToString()
{
return Success ? $"Success" : $"Error: {Error}";
}
}
/// <summary>
@@ -155,6 +193,11 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? RequestHeaders { get; set; }
/// <summary>
/// The request id
/// </summary>
public int? RequestId { get; set; }
/// <summary>
/// The url which was requested
/// </summary>
@@ -186,6 +229,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="code"></param>
/// <param name="responseHeaders"></param>
/// <param name="responseTime"></param>
/// <param name="requestId"></param>
/// <param name="requestUrl"></param>
/// <param name="requestBody"></param>
/// <param name="requestMethod"></param>
@@ -195,6 +239,7 @@ namespace CryptoExchange.Net.Objects
HttpStatusCode? code,
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders,
TimeSpan? responseTime,
int? requestId,
string? requestUrl,
string? requestBody,
HttpMethod? requestMethod,
@@ -204,6 +249,7 @@ namespace CryptoExchange.Net.Objects
ResponseStatusCode = code;
ResponseHeaders = responseHeaders;
ResponseTime = responseTime;
RequestId = requestId;
RequestUrl = requestUrl;
RequestBody = requestBody;
@@ -224,7 +270,13 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
public WebCallResult AsError(Error error)
{
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
}
/// <inheritdoc />
public override string ToString()
{
return (Success ? $"Success" : $"Error: {Error}") + $" in {ResponseTime}";
}
}
@@ -244,6 +296,11 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? RequestHeaders { get; set; }
/// <summary>
/// The request id
/// </summary>
public int? RequestId { get; set; }
/// <summary>
/// The url which was requested
/// </summary>
@@ -259,6 +316,11 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public HttpStatusCode? ResponseStatusCode { get; set; }
/// <summary>
/// Length in bytes of the response
/// </summary>
public long? ResponseLength { get; set; }
/// <summary>
/// The response headers
/// </summary>
@@ -275,7 +337,9 @@ namespace CryptoExchange.Net.Objects
/// <param name="code"></param>
/// <param name="responseHeaders"></param>
/// <param name="responseTime"></param>
/// <param name="responseLength"></param>
/// <param name="originalData"></param>
/// <param name="requestId"></param>
/// <param name="requestUrl"></param>
/// <param name="requestBody"></param>
/// <param name="requestMethod"></param>
@@ -286,7 +350,9 @@ namespace CryptoExchange.Net.Objects
HttpStatusCode? code,
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders,
TimeSpan? responseTime,
long? responseLength,
string? originalData,
int? requestId,
string? requestUrl,
string? requestBody,
HttpMethod? requestMethod,
@@ -297,18 +363,37 @@ namespace CryptoExchange.Net.Objects
ResponseStatusCode = code;
ResponseHeaders = responseHeaders;
ResponseTime = responseTime;
ResponseLength = responseLength;
RequestId = requestId;
RequestUrl = requestUrl;
RequestBody = requestBody;
RequestHeaders = requestHeaders;
RequestMethod = requestMethod;
}
/// <summary>
/// Copy as a dataless result
/// </summary>
/// <returns></returns>
public new WebCallResult AsDataless()
{
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error);
}
/// <summary>
/// Copy as a dataless result
/// </summary>
/// <returns></returns>
public new WebCallResult AsDatalessError(Error error)
{
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
}
/// <summary>
/// Create a new error result
/// </summary>
/// <param name="error">The error</param>
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, default, error) { }
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, default, error) { }
/// <summary>
/// Copy the WebCallResult to a new data type
@@ -318,25 +403,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
public new WebCallResult<K> As<K>([AllowNull] K data)
{
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestUrl, RequestBody, RequestMethod, RequestHeaders, data, Error);
}
/// <summary>
/// Copy as a dataless result
/// </summary>
/// <returns></returns>
public WebCallResult AsDataless()
{
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error);
}
/// <summary>
/// Copy as a dataless result
/// </summary>
/// <returns></returns>
public WebCallResult AsDatalessError(Error error)
{
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, data, Error);
}
/// <summary>
@@ -347,7 +414,20 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
public new WebCallResult<K> AsError<K>(Error error)
{
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, error);
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, error);
}
/// <inheritdoc />
public override string ToString()
{
var sb = new StringBuilder();
sb.Append(Success ? $"Success response" : $"Error response: {Error}");
if (ResponseLength != null)
sb.Append($", {ResponseLength} bytes");
if (ResponseTime != null)
sb.Append($" received in {Math.Round(ResponseTime?.TotalMilliseconds ?? 0)}ms");
return sb.ToString();
}
}
}
+15
View File
@@ -124,4 +124,19 @@
/// </summary>
Closest
}
/// <summary>
/// Type of the update
/// </summary>
public enum SocketUpdateType
{
/// <summary>
/// A update
/// </summary>
Update,
/// <summary>
/// A snapshot, generally send at the start of the connection
/// </summary>
Snapshot
}
}
+129 -10
View File
@@ -1,4 +1,6 @@
namespace CryptoExchange.Net.Objects
using System;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Base class for errors
@@ -39,7 +41,7 @@
/// <returns></returns>
public override string ToString()
{
return $"{Code}: {Message} {Data}";
return Code != null ? $"[{GetType().Name}] {Code}: {Message} {Data}" : $"[{GetType().Name}] {Message} {Data}";
}
}
@@ -52,6 +54,14 @@
/// ctor
/// </summary>
public CantConnectError() : base(null, "Can't connect to the server", null) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected CantConnectError(int? code, string message, object? data) : base(code, message, data) { }
}
/// <summary>
@@ -63,12 +73,20 @@
/// ctor
/// </summary>
public NoApiCredentialsError() : base(null, "No credentials provided for private endpoint", null) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected NoApiCredentialsError(int? code, string message, object? data) : base(code, message, data) { }
}
/// <summary>
/// Error returned by the server
/// </summary>
public class ServerError: Error
public class ServerError : Error
{
/// <summary>
/// ctor
@@ -83,9 +101,15 @@
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
public ServerError(int code, string message, object? data = null) : base(code, message, data)
{
}
public ServerError(int code, string message, object? data = null) : base(code, message, data) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected ServerError(int? code, string message, object? data) : base(code, message, data) { }
}
/// <summary>
@@ -107,6 +131,14 @@
/// <param name="message"></param>
/// <param name="data"></param>
public WebError(int code, string message, object? data = null) : base(code, message, data) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected WebError(int? code, string message, object? data): base(code, message, data) { }
}
/// <summary>
@@ -120,6 +152,14 @@
/// <param name="message">The error message</param>
/// <param name="data">The data which caused the error</param>
public DeserializeError(string message, object? data) : base(null, message, data) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected DeserializeError(int? code, string message, object? data): base(code, message, data) { }
}
/// <summary>
@@ -133,6 +173,14 @@
/// <param name="message">Error message</param>
/// <param name="data">Error data</param>
public UnknownError(string message, object? data = null) : base(null, message, data) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected UnknownError(int? code, string message, object? data): base(code, message, data) { }
}
/// <summary>
@@ -145,18 +193,73 @@
/// </summary>
/// <param name="message"></param>
public ArgumentError(string message) : base(null, "Invalid parameter: " + message, null) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected ArgumentError(int? code, string message, object? data): base(code, message, data) { }
}
/// <summary>
/// Rate limit exceeded
/// Rate limit exceeded (client side)
/// </summary>
public class RateLimitError: Error
public abstract class BaseRateLimitError : Error
{
/// <summary>
/// When the request can be retried
/// </summary>
public DateTime? RetryAfter { get; set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected BaseRateLimitError(int? code, string message, object? data) : base(code, message, data) { }
}
/// <summary>
/// Rate limit exceeded (client side)
/// </summary>
public class ClientRateLimitError : BaseRateLimitError
{
/// <summary>
/// ctor
/// </summary>
/// <param name="message"></param>
public RateLimitError(string message) : base(null, "Rate limit exceeded: " + message, null) { }
public ClientRateLimitError(string message) : base(null, "Client rate limit exceeded: " + message, null) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected ClientRateLimitError(int? code, string message, object? data): base(code, message, data) { }
}
/// <summary>
/// Rate limit exceeded (server side)
/// </summary>
public class ServerRateLimitError : BaseRateLimitError
{
/// <summary>
/// ctor
/// </summary>
/// <param name="message"></param>
public ServerRateLimitError(string message) : base(null, "Server rate limit exceeded: " + message, null) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected ServerRateLimitError(int? code, string message, object? data) : base(code, message, data) { }
}
/// <summary>
@@ -168,17 +271,33 @@
/// ctor
/// </summary>
public CancellationRequestedError() : base(null, "Cancellation requested", null) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
public CancellationRequestedError(int? code, string message, object? data): base(code, message, data) { }
}
/// <summary>
/// Invalid operation requested
/// </summary>
public class InvalidOperationError: Error
public class InvalidOperationError : Error
{
/// <summary>
/// ctor
/// </summary>
/// <param name="message"></param>
public InvalidOperationError(string message) : base(null, message, null) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected InvalidOperationError(int? code, string message, object? data): base(code, message, data) { }
}
}
-329
View File
@@ -1,329 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Client options
/// </summary>
public abstract class ClientOptions
{
internal event Action? OnLoggingChanged;
private LogLevel _logLevel = LogLevel.Information;
/// <summary>
/// The minimum log level to output
/// </summary>
public LogLevel LogLevel
{
get => _logLevel;
set
{
_logLevel = value;
OnLoggingChanged?.Invoke();
}
}
private List<ILogger> _logWriters = new List<ILogger> { new DebugLogger() };
/// <summary>
/// The log writers
/// </summary>
public List<ILogger> LogWriters
{
get => _logWriters;
set
{
_logWriters = value;
OnLoggingChanged?.Invoke();
}
}
/// <summary>
/// Proxy to use when connecting
/// </summary>
public ApiProxy? Proxy { get; set; }
/// <summary>
/// The api credentials used for signing requests to this API.
/// </summary>
public ApiCredentials? ApiCredentials { get; set; }
/// <summary>
/// ctor
/// </summary>
public ClientOptions()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="clientOptions">Copy values for the provided options</param>
public ClientOptions(ClientOptions? clientOptions)
{
if (clientOptions == null)
return;
LogLevel = clientOptions.LogLevel;
LogWriters = clientOptions.LogWriters.ToList();
Proxy = clientOptions.Proxy;
ApiCredentials = clientOptions.ApiCredentials?.Copy();
}
/// <summary>
/// ctor
/// </summary>
/// <param name="baseOptions">Copy values for the provided options</param>
/// <param name="newValues">Copy values for the provided options</param>
internal ClientOptions(ClientOptions baseOptions, ClientOptions? newValues)
{
Proxy = newValues?.Proxy ?? baseOptions.Proxy;
LogLevel = baseOptions.LogLevel;
LogWriters = baseOptions.LogWriters.ToList();
ApiCredentials = newValues?.ApiCredentials?.Copy() ?? baseOptions.ApiCredentials?.Copy();
}
/// <inheritdoc />
public override string ToString()
{
return $"LogLevel: {LogLevel}, Writers: {LogWriters.Count}, Proxy: {(Proxy == null ? "-" : Proxy.Host)}";
}
}
/// <summary>
/// API client options
/// </summary>
public class ApiClientOptions
{
/// <summary>
/// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property
/// </summary>
public bool OutputOriginalData { get; set; } = false;
/// <summary>
/// The base address of the API
/// </summary>
public string BaseAddress { get; set; }
/// <summary>
/// The api credentials used for signing requests to this API. Overrides API credentials provided in the client options
/// </summary>
public ApiCredentials? ApiCredentials { get; set; }
/// <summary>
/// ctor
/// </summary>
#pragma warning disable 8618 // Will always get filled by the implementation
public ApiClientOptions()
{
}
#pragma warning restore 8618
/// <summary>
/// ctor
/// </summary>
/// <param name="baseAddress">Base address for the API</param>
public ApiClientOptions(string baseAddress)
{
BaseAddress = baseAddress;
}
/// <summary>
/// ctor
/// </summary>
/// <param name="baseOptions">Copy values for the provided options</param>
/// <param name="newValues">Copy values for the provided options</param>
public ApiClientOptions(ApiClientOptions baseOptions, ApiClientOptions? newValues)
{
BaseAddress = newValues?.BaseAddress ?? baseOptions.BaseAddress;
ApiCredentials = newValues?.ApiCredentials?.Copy() ?? baseOptions.ApiCredentials?.Copy();
OutputOriginalData = newValues?.OutputOriginalData ?? baseOptions.OutputOriginalData;
}
/// <inheritdoc />
public override string ToString()
{
return $"OutputOriginalData: {OutputOriginalData}, Credentials: {(ApiCredentials == null ? "-" : "Set")}, BaseAddress: {BaseAddress}";
}
}
/// <summary>
/// Rest API client options
/// </summary>
public class RestApiClientOptions: ApiClientOptions
{
/// <summary>
/// The time the server has to respond to a request before timing out
/// </summary>
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(30);
/// <summary>
/// Http client to use. If a HttpClient is provided in this property the RequestTimeout and Proxy options provided in these options will be ignored in requests and should be set on the provided HttpClient instance
/// </summary>
public HttpClient? HttpClient { get; set; }
/// <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>
/// Whether or not to automatically sync the local time with the server time
/// </summary>
public bool AutoTimestamp { get; set; }
/// <summary>
/// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
/// </summary>
public TimeSpan TimestampRecalculationInterval { get; set; } = TimeSpan.FromHours(1);
/// <summary>
/// ctor
/// </summary>
public RestApiClientOptions()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="baseAddress">Base address for the API</param>
public RestApiClientOptions(string baseAddress): base(baseAddress)
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="baseOn">Copy values for the provided options</param>
/// <param name="newValues">Copy values for the provided options</param>
public RestApiClientOptions(RestApiClientOptions baseOn, RestApiClientOptions? newValues): base(baseOn, newValues)
{
HttpClient = newValues?.HttpClient ?? baseOn.HttpClient;
RequestTimeout = newValues == default ? baseOn.RequestTimeout : newValues.RequestTimeout;
RateLimitingBehaviour = newValues?.RateLimitingBehaviour ?? baseOn.RateLimitingBehaviour;
AutoTimestamp = newValues?.AutoTimestamp ?? baseOn.AutoTimestamp;
TimestampRecalculationInterval = newValues?.TimestampRecalculationInterval ?? baseOn.TimestampRecalculationInterval;
RateLimiters = newValues?.RateLimiters.ToList() ?? baseOn?.RateLimiters.ToList() ?? new List<IRateLimiter>();
}
/// <inheritdoc />
public override string ToString()
{
return $"{base.ToString()}, RequestTimeout: {RequestTimeout:c}, HttpClient: {(HttpClient == null ? "-" : "set")}, RateLimiters: {RateLimiters?.Count}, RateLimitBehaviour: {RateLimitingBehaviour}, AutoTimestamp: {AutoTimestamp}, TimestampRecalculationInterval: {TimestampRecalculationInterval}";
}
}
/// <summary>
/// Rest API client options
/// </summary>
public class SocketApiClientOptions : ApiClientOptions
{
/// <summary>
/// Whether or not the socket should automatically reconnect when losing connection
/// </summary>
public bool AutoReconnect { get; set; } = true;
/// <summary>
/// Time to wait between reconnect attempts
/// </summary>
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
/// <summary>
/// Max number of concurrent resubscription tasks per socket after reconnecting a socket
/// </summary>
public int MaxConcurrentResubscriptionsPerSocket { get; set; } = 5;
/// <summary>
/// The max time to wait for a response after sending a request on the socket before giving a timeout
/// </summary>
public TimeSpan SocketResponseTimeout { get; set; } = TimeSpan.FromSeconds(10);
/// <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,
/// for example when the server sends intermittent ping requests
/// </summary>
public TimeSpan SocketNoDataTimeout { get; set; }
/// <summary>
/// The amount of subscriptions that should be made on a single socket connection. Not all API's support multiple subscriptions on a single socket.
/// Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a
/// single connection will also increase the amount of traffic on that single connection, potentially leading to issues.
/// </summary>
public int? SocketSubscriptionsCombineTarget { get; set; }
/// <summary>
/// The max amount of connections to make to the server. Can be used for API's which only allow a certain number of connections. Changing this to a high value might cause issues.
/// </summary>
public int? MaxSocketConnections { get; set; }
/// <summary>
/// The time to wait after connecting a socket before sending messages. Can be used for API's which will rate limit if you subscribe directly after connecting.
/// </summary>
public TimeSpan DelayAfterConnect { get; set; } = TimeSpan.Zero;
/// <summary>
/// ctor
/// </summary>
public SocketApiClientOptions()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="baseAddress">Base address for the API</param>
public SocketApiClientOptions(string baseAddress) : base(baseAddress)
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="baseOptions">Copy values for the provided options</param>
/// <param name="newValues">Copy values for the provided options</param>
public SocketApiClientOptions(SocketApiClientOptions baseOptions, SocketApiClientOptions? newValues) : base(baseOptions, newValues)
{
if (baseOptions == null)
return;
AutoReconnect = newValues?.AutoReconnect ?? baseOptions.AutoReconnect;
ReconnectInterval = newValues?.ReconnectInterval ?? baseOptions.ReconnectInterval;
MaxConcurrentResubscriptionsPerSocket = newValues?.MaxConcurrentResubscriptionsPerSocket ?? baseOptions.MaxConcurrentResubscriptionsPerSocket;
SocketResponseTimeout = newValues?.SocketResponseTimeout ?? baseOptions.SocketResponseTimeout;
SocketNoDataTimeout = newValues?.SocketNoDataTimeout ?? baseOptions.SocketNoDataTimeout;
SocketSubscriptionsCombineTarget = newValues?.SocketSubscriptionsCombineTarget ?? baseOptions.SocketSubscriptionsCombineTarget;
MaxSocketConnections = newValues?.MaxSocketConnections ?? baseOptions.MaxSocketConnections;
DelayAfterConnect = newValues?.DelayAfterConnect ?? baseOptions.DelayAfterConnect;
}
/// <inheritdoc />
public override string ToString()
{
return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, MaxConcurrentResubscriptionsPerSocket: {MaxConcurrentResubscriptionsPerSocket}, SocketResponseTimeout: {SocketResponseTimeout:c}, SocketNoDataTimeout: {SocketNoDataTimeout}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}, MaxSocketConnections: {MaxSocketConnections}";
}
}
/// <summary>
/// Base for order book options
/// </summary>
public class OrderBookOptions : ClientOptions
{
/// <summary>
/// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages.
/// </summary>
public bool ChecksumValidationEnabled { get; set; } = true;
}
}
@@ -0,0 +1,20 @@
using CryptoExchange.Net.Authentication;
namespace CryptoExchange.Net.Objects.Options
{
/// <summary>
/// Options for API usage
/// </summary>
public class ApiOptions
{
/// <summary>
/// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property
/// </summary>
public bool? OutputOriginalData { get; set; }
/// <summary>
/// The api credentials used for signing requests to this API. Overrides API credentials provided in the client options
/// </summary>
public ApiCredentials? ApiCredentials { get; set; }
}
}
@@ -0,0 +1,37 @@
using CryptoExchange.Net.Authentication;
using System;
namespace CryptoExchange.Net.Objects.Options
{
/// <summary>
/// Exchange options
/// </summary>
public class ExchangeOptions
{
/// <summary>
/// Proxy settings
/// </summary>
public ApiProxy? Proxy { get; set; }
/// <summary>
/// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property
/// </summary>
public bool OutputOriginalData { get; set; } = false;
/// <summary>
/// The max time a request is allowed to take
/// </summary>
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(20);
/// <summary>
/// The api credentials used for signing requests to this API.
/// </summary>
public ApiCredentials? ApiCredentials { get; set; }
/// <inheritdoc />
public override string ToString()
{
return $"RequestTimeout: {RequestTimeout}, Proxy: {(Proxy == null ? "-" : "set")}, ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
}
}
}
@@ -0,0 +1,30 @@
namespace CryptoExchange.Net.Objects.Options
{
/// <summary>
/// Base for order book options
/// </summary>
public class OrderBookOptions : ExchangeOptions
{
/// <summary>
/// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages.
/// </summary>
public bool ChecksumValidationEnabled { get; set; } = true;
/// <summary>
/// Create a copy of this options
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T Copy<T>() where T : OrderBookOptions, new()
{
return new T
{
ApiCredentials = ApiCredentials?.Copy(),
OutputOriginalData = OutputOriginalData,
ChecksumValidationEnabled = ChecksumValidationEnabled,
Proxy = Proxy,
RequestTimeout = RequestTimeout
};
}
}
}
@@ -0,0 +1,67 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.Objects.Options
{
/// <summary>
/// Http api options
/// </summary>
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>
/// Whether or not to automatically sync the local time with the server time
/// </summary>
public bool? AutoTimestamp { get; set; }
/// <summary>
/// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
/// </summary>
public TimeSpan? TimestampRecalculationInterval { get; set; }
/// <summary>
/// Create a copy of this options
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public virtual T Copy<T>() where T : RestApiOptions, new()
{
return new T
{
ApiCredentials = ApiCredentials?.Copy(),
OutputOriginalData = OutputOriginalData,
AutoTimestamp = AutoTimestamp,
RateLimiters = RateLimiters,
RateLimitingBehaviour = RateLimitingBehaviour,
TimestampRecalculationInterval = TimestampRecalculationInterval
};
}
}
/// <summary>
/// Http API options
/// </summary>
/// <typeparam name="TApiCredentials"></typeparam>
public class RestApiOptions<TApiCredentials>: RestApiOptions where TApiCredentials: ApiCredentials
{
/// <summary>
/// The api credentials used for signing requests to this API.
/// </summary>
public new TApiCredentials? ApiCredentials
{
get => (TApiCredentials?)base.ApiCredentials;
set => base.ApiCredentials = value;
}
}
}
@@ -0,0 +1,83 @@
using CryptoExchange.Net.Authentication;
using System;
namespace CryptoExchange.Net.Objects.Options
{
/// <summary>
/// Options for a rest exchange client
/// </summary>
public class RestExchangeOptions: ExchangeOptions
{
/// <summary>
/// Whether or not to automatically sync the local time with the server time
/// </summary>
public bool AutoTimestamp { get; set; }
/// <summary>
/// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
/// </summary>
public TimeSpan TimestampRecalculationInterval { get; set; } = TimeSpan.FromHours(1);
/// <summary>
/// Create a copy of this options
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T Copy<T>() where T : RestExchangeOptions, new()
{
return new T
{
OutputOriginalData = OutputOriginalData,
AutoTimestamp = AutoTimestamp,
TimestampRecalculationInterval = TimestampRecalculationInterval,
ApiCredentials = ApiCredentials?.Copy(),
Proxy = Proxy,
RequestTimeout = RequestTimeout
};
}
}
/// <summary>
/// Options for a rest exchange client
/// </summary>
/// <typeparam name="TEnvironment"></typeparam>
public class RestExchangeOptions<TEnvironment> : RestExchangeOptions where TEnvironment : TradeEnvironment
{
/// <summary>
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
/// the exhange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
/// </summary>
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
public TEnvironment Environment { get; set; }
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
/// <summary>
/// Create a copy of this options
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public new T Copy<T>() where T : RestExchangeOptions<TEnvironment>, new()
{
var result = base.Copy<T>();
result.Environment = Environment;
return result;
}
}
/// <summary>
/// Options for a rest exchange client
/// </summary>
/// <typeparam name="TEnvironment"></typeparam>
/// <typeparam name="TApiCredentials"></typeparam>
public class RestExchangeOptions<TEnvironment, TApiCredentials> : RestExchangeOptions<TEnvironment> where TEnvironment : TradeEnvironment where TApiCredentials : ApiCredentials
{
/// <summary>
/// The api credentials used for signing requests to this API.
/// </summary>
public new TApiCredentials? ApiCredentials
{
get => (TApiCredentials?)base.ApiCredentials;
set => base.ApiCredentials = value;
}
}
}
@@ -0,0 +1,62 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.Objects.Options
{
/// <summary>
/// Socket api options
/// </summary>
public class SocketApiOptions : ApiOptions
{
/// <summary>
/// List of rate limiters to use
/// </summary>
public List<IRateLimiter> RateLimiters { get; set; } = new List<IRateLimiter>();
/// <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,
/// for example when the server sends intermittent ping requests
/// </summary>
public TimeSpan? SocketNoDataTimeout { get; set; }
/// <summary>
/// The max amount of connections to make to the server. Can be used for API's which only allow a certain number of connections. Changing this to a high value might cause issues.
/// </summary>
public int? MaxSocketConnections { get; set; }
/// <summary>
/// Create a copy of this options
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T Copy<T>() where T : SocketApiOptions, new()
{
return new T
{
ApiCredentials = ApiCredentials?.Copy(),
OutputOriginalData = OutputOriginalData,
RateLimiters = RateLimiters,
SocketNoDataTimeout = SocketNoDataTimeout,
MaxSocketConnections = MaxSocketConnections,
};
}
}
/// <summary>
/// Socket API options
/// </summary>
/// <typeparam name="TApiCredentials"></typeparam>
public class SocketApiOptions<TApiCredentials> : SocketApiOptions where TApiCredentials : ApiCredentials
{
/// <summary>
/// The api credentials used for signing requests to this API.
/// </summary>
public new TApiCredentials? ApiCredentials
{
get => (TApiCredentials?)base.ApiCredentials;
set => base.ApiCredentials = value;
}
}
}
@@ -0,0 +1,116 @@
using CryptoExchange.Net.Authentication;
using System;
namespace CryptoExchange.Net.Objects.Options
{
/// <summary>
/// Options for a websocket exchange client
/// </summary>
public class SocketExchangeOptions : ExchangeOptions
{
/// <summary>
/// Whether or not the socket should automatically reconnect when losing connection
/// </summary>
public bool AutoReconnect { get; set; } = true;
/// <summary>
/// Time to wait between reconnect attempts
/// </summary>
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
/// <summary>
/// Max number of concurrent resubscription tasks per socket after reconnecting a socket
/// </summary>
public int MaxConcurrentResubscriptionsPerSocket { get; set; } = 5;
/// <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,
/// for example when the server sends intermittent ping requests
/// </summary>
public TimeSpan SocketNoDataTimeout { get; set; }
/// <summary>
/// The amount of subscriptions that should be made on a single socket connection. Not all API's support multiple subscriptions on a single socket.
/// Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a
/// single connection will also increase the amount of traffic on that single connection, potentially leading to issues.
/// </summary>
public int? SocketSubscriptionsCombineTarget { get; set; }
/// <summary>
/// The max amount of connections to make to the server. Can be used for API's which only allow a certain number of connections. Changing this to a high value might cause issues.
/// </summary>
public int? MaxSocketConnections { get; set; }
/// <summary>
/// The time to wait after connecting a socket before sending messages. Can be used for API's which will rate limit if you subscribe directly after connecting.
/// </summary>
public TimeSpan DelayAfterConnect { get; set; } = TimeSpan.Zero;
/// <summary>
/// Create a copy of this options
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T Copy<T>() where T : SocketExchangeOptions, new()
{
return new T
{
ApiCredentials = ApiCredentials?.Copy(),
OutputOriginalData = OutputOriginalData,
AutoReconnect = AutoReconnect,
DelayAfterConnect = DelayAfterConnect,
MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket,
ReconnectInterval = ReconnectInterval,
SocketNoDataTimeout = SocketNoDataTimeout,
SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget,
MaxSocketConnections = MaxSocketConnections,
Proxy = Proxy,
RequestTimeout = RequestTimeout
};
}
}
/// <summary>
/// Options for a socket exchange client
/// </summary>
/// <typeparam name="TEnvironment"></typeparam>
public class SocketExchangeOptions<TEnvironment> : SocketExchangeOptions where TEnvironment : TradeEnvironment
{
/// <summary>
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
/// the exhange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
/// </summary>
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
public TEnvironment Environment { get; set; }
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
/// <summary>
/// Create a copy of this options
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public new T Copy<T>() where T : SocketExchangeOptions<TEnvironment>, new()
{
var result = base.Copy<T>();
result.Environment = Environment;
return result;
}
}
/// <summary>
/// Options for a socket exchange client
/// </summary>
/// <typeparam name="TEnvironment"></typeparam>
/// <typeparam name="TApiCredentials"></typeparam>
public class SocketExchangeOptions<TEnvironment, TApiCredentials> : SocketExchangeOptions<TEnvironment> where TEnvironment : TradeEnvironment where TApiCredentials : ApiCredentials
{
/// <summary>
/// The api credentials used for signing requests to this API.
/// </summary>
public new TApiCredentials? ApiCredentials
{
get => (TApiCredentials?)base.ApiCredentials;
set => base.ApiCredentials = value;
}
}
}
@@ -0,0 +1,172 @@
using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Converters;
using System;
using System.Collections.Generic;
using System.Globalization;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Parameters collection
/// </summary>
public class ParameterCollection : Dictionary<string, object>
{
/// <summary>
/// Add an optional parameter. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddOptional(string key, object? value)
{
if (value != null)
Add(key, value);
}
/// <summary>
/// Add a decimal value as string
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddString(string key, decimal value)
{
Add(key, value.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Add a decimal value as string. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddOptionalString(string key, decimal? value)
{
if (value != null)
Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Add a int value as string
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddString(string key, int value)
{
Add(key, value.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Add a int value as string. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddOptionalString(string key, int? value)
{
if (value != null)
Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Add a long value as string
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddString(string key, long value)
{
Add(key, value.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Add a long value as string. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddOptionalString(string key, long? value)
{
if (value != null)
Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Add a datetime value as milliseconds timestamp
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddMilliseconds(string key, DateTime value)
{
Add(key, DateTimeConverter.ConvertToMilliseconds(value));
}
/// <summary>
/// Add a datetime value as milliseconds timestamp. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddOptionalMilliseconds(string key, DateTime? value)
{
if (value != null)
Add(key, DateTimeConverter.ConvertToMilliseconds(value));
}
/// <summary>
/// Add a datetime value as milliseconds timestamp
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddMillisecondsString(string key, DateTime value)
{
Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Add a datetime value as milliseconds timestamp. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddOptionalMillisecondsString(string key, DateTime? value)
{
if (value != null)
Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Add a datetime value as seconds timestamp
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddSeconds(string key, DateTime value)
{
Add(key, DateTimeConverter.ConvertToSeconds(value));
}
/// <summary>
/// Add a datetime value as seconds timestamp. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddOptionalSeconds(string key, DateTime? value)
{
if (value != null)
Add(key, DateTimeConverter.ConvertToSeconds(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 AddEnum<T>(string key, T 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 AddOptionalEnum<T>(string key, T? value)
{
if (value != null)
Add(key, EnumConverter.GetString(value));
}
}
}
+114 -79
View File
@@ -1,5 +1,4 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
@@ -18,7 +17,7 @@ namespace CryptoExchange.Net.Objects
public class RateLimiter : IRateLimiter
{
private readonly object _limiterLock = new object();
internal List<Limiter> Limiters = new List<Limiter>();
internal List<Limiter> _limiters = new List<Limiter>();
/// <summary>
/// Create a new RateLimiter. Configure the rate limiter by calling <see cref="AddTotalRateLimit"/>,
@@ -36,7 +35,7 @@ namespace CryptoExchange.Net.Objects
public RateLimiter AddTotalRateLimit(int limit, TimeSpan perTimePeriod)
{
lock(_limiterLock)
Limiters.Add(new TotalRateLimiter(limit, perTimePeriod, null));
_limiters.Add(new TotalRateLimiter(limit, perTimePeriod, null));
return this;
}
@@ -51,7 +50,7 @@ namespace CryptoExchange.Net.Objects
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));
_limiters.Add(new EndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, excludeFromOtherRateLimits));
return this;
}
@@ -66,7 +65,7 @@ namespace CryptoExchange.Net.Objects
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));
_limiters.Add(new EndpointRateLimiter(endpoints.ToArray(), limit, perTimePeriod, method, excludeFromOtherRateLimits));
return this;
}
@@ -82,7 +81,7 @@ namespace CryptoExchange.Net.Objects
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));
_limiters.Add(new PartialEndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, ignoreOtherRateLimits, countPerEndpoint));
return this;
}
@@ -96,33 +95,46 @@ namespace CryptoExchange.Net.Objects
public RateLimiter AddApiKeyLimit(int limit, TimeSpan perTimePeriod, bool onlyForSignedRequests, bool excludeFromTotalRateLimit)
{
lock(_limiterLock)
Limiters.Add(new ApiKeyRateLimiter(limit, perTimePeriod, null, onlyForSignedRequests, excludeFromTotalRateLimit));
_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(Log log, string endpoint, HttpMethod method, bool signed, SecureString? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct)
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;
EndpointRateLimiter? endpointLimit;
List<EndpointRateLimiter> endpointLimits;
lock (_limiterLock)
endpointLimit = Limiters.OfType<EndpointRateLimiter>().SingleOrDefault(h => h.Endpoints.Contains(endpoint) && (h.Method == null || h.Method == method));
if(endpointLimit != null)
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(log, endpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
var waitResult = await ProcessTopic(logger, endpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
if (!waitResult)
return waitResult;
totalWaitTime += waitResult.Data;
}
if (endpointLimit?.IgnoreOtherRateLimits == true)
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();
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)
@@ -130,15 +142,15 @@ namespace CryptoExchange.Net.Objects
SingleTopicRateLimiter? thisEndpointLimit;
lock (_limiterLock)
{
thisEndpointLimit = Limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.PartialEndpoint && (string)h.Topic == endpoint);
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);
_limiters.Add(thisEndpointLimit);
}
}
var waitResult = await ProcessTopic(log, thisEndpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
var waitResult = await ProcessTopic(logger, thisEndpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
if (!waitResult)
return waitResult;
@@ -146,7 +158,7 @@ namespace CryptoExchange.Net.Objects
}
else
{
var waitResult = await ProcessTopic(log, partialEndpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
var waitResult = await ProcessTopic(logger, partialEndpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
if (!waitResult)
return waitResult;
@@ -157,16 +169,16 @@ namespace CryptoExchange.Net.Objects
if(partialEndpointLimits.Any(p => p.IgnoreOtherRateLimits))
return new CallResult<int>(totalWaitTime);
ApiKeyRateLimiter? apiLimit;
List<ApiKeyRateLimiter> apiLimits;
lock (_limiterLock)
apiLimit = Limiters.OfType<ApiKeyRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey);
if (apiLimit != null)
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(log, apiLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
var waitResult = await ProcessTopic(logger, apiLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
if (!waitResult)
return waitResult;
@@ -178,15 +190,15 @@ namespace CryptoExchange.Net.Objects
SingleTopicRateLimiter? thisApiLimit;
lock (_limiterLock)
{
thisApiLimit = Limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey && ((SecureString)h.Topic).IsEqualTo(apiKey));
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);
_limiters.Add(thisApiLimit);
}
}
var waitResult = await ProcessTopic(log, thisApiLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
var waitResult = await ProcessTopic(logger, thisApiLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
if (!waitResult)
return waitResult;
@@ -194,15 +206,15 @@ namespace CryptoExchange.Net.Objects
}
}
if ((signed || apiLimit?.OnlyForSignedRequests == false) && apiLimit?.IgnoreTotalRateLimit == true)
if ((signed || apiLimits.All(l => !l.OnlyForSignedRequests)) && apiLimits.Any(l => l.IgnoreTotalRateLimit))
return new CallResult<int>(totalWaitTime);
TotalRateLimiter? totalLimit;
List<TotalRateLimiter> totalLimits;
lock (_limiterLock)
totalLimit = Limiters.OfType<TotalRateLimiter>().SingleOrDefault();
if (totalLimit != null)
totalLimits = _limiters.OfType<TotalRateLimiter>().ToList();
foreach(var totalLimit in totalLimits)
{
var waitResult = await ProcessTopic(log, totalLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
var waitResult = await ProcessTopic(logger, totalLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
if (!waitResult)
return waitResult;
@@ -212,7 +224,7 @@ namespace CryptoExchange.Net.Objects
return new CallResult<int>(totalWaitTime);
}
private static async Task<CallResult<int>> ProcessTopic(Log log, Limiter historyTopic, string endpoint, int requestWeight, RateLimitingBehaviour limitBehaviour, CancellationToken ct)
private static async Task<CallResult<int>> ProcessTopic(ILogger logger, Limiter historyTopic, string endpoint, int requestWeight, RateLimitingBehaviour limitBehaviour, CancellationToken ct)
{
var sw = Stopwatch.StartNew();
try
@@ -225,63 +237,68 @@ namespace CryptoExchange.Net.Objects
}
sw.Stop();
int totalWaitTime = 0;
while (true)
try
{
// Remove requests no longer in time period from the history
var checkTime = DateTime.UtcNow;
for (var i = 0; i < historyTopic.Entries.Count; i++)
int totalWaitTime = 0;
while (true)
{
if (historyTopic.Entries[i].Timestamp < checkTime - historyTopic.Period)
// Remove requests no longer in time period from the history
var checkTime = DateTime.UtcNow;
for (var i = 0; i < historyTopic.Entries.Count; i++)
{
historyTopic.Entries.Remove(historyTopic.Entries[i]);
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 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 - (checkTime - historyTopic.Period)).TotalMilliseconds);
if (thisWaitTime > 0)
{
if (limitBehaviour == RateLimitingBehaviour.Fail)
{
historyTopic.Semaphore.Release();
var msg = $"Request to {endpoint} failed because of rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}";
log.Write(LogLevel.Warning, msg);
return new CallResult<int>(new RateLimitError(msg));
}
log.Write(LogLevel.Information, $"Request 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;
break;
}
}
else
{
break;
}
}
var newTime = DateTime.UtcNow;
historyTopic.Entries.Add(new LimitEntry(newTime, requestWeight));
historyTopic.Semaphore.Release();
return new CallResult<int>(totalWaitTime);
var newTime = DateTime.UtcNow;
historyTopic.Entries.Add(new LimitEntry(newTime, requestWeight));
return new CallResult<int>(totalWaitTime);
}
finally
{
historyTopic.Semaphore.Release();
}
}
internal struct LimitEntry
@@ -330,6 +347,24 @@ namespace CryptoExchange.Net.Objects
}
}
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; }
@@ -1,6 +1,6 @@
using System;
namespace CryptoExchange.Net.Sockets
namespace CryptoExchange.Net.Objects.Sockets
{
/// <summary>
/// An update received from a socket update subscription
@@ -12,43 +12,34 @@ namespace CryptoExchange.Net.Sockets
/// The timestamp the data was received
/// </summary>
public DateTime Timestamp { get; set; }
/// <summary>
/// The topic of the update, what symbol/asset etc..
/// </summary>
public string? Topic { get; set; }
/// <summary>
/// The original data that was received, only available when OutputOriginalData is set to true in the client options
/// </summary>
public string? OriginalData { get; set; }
/// <summary>
/// Type of update
/// </summary>
public SocketUpdateType? UpdateType { get; set; }
/// <summary>
/// The received data deserialized into an object
/// </summary>
public T Data { get; set; }
/// <summary>
/// Ctor
/// </summary>
/// <param name="data"></param>
/// <param name="timestamp"></param>
public DataEvent(T data, DateTime timestamp)
{
Data = data;
Timestamp = timestamp;
}
internal DataEvent(T data, string? topic, DateTime timestamp)
{
Data = data;
Topic = topic;
Timestamp = timestamp;
}
internal DataEvent(T data, string? topic, string? originalData, DateTime timestamp)
internal DataEvent(T data, string? topic, string? originalData, DateTime timestamp, SocketUpdateType? updateType)
{
Data = data;
Topic = topic;
OriginalData = originalData;
Timestamp = timestamp;
UpdateType = updateType;
}
/// <summary>
@@ -59,7 +50,7 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns>
public DataEvent<K> As<K>(K data)
{
return new DataEvent<K>(data, Topic, OriginalData, Timestamp);
return new DataEvent<K>(data, Topic, OriginalData, Timestamp, UpdateType);
}
/// <summary>
@@ -71,7 +62,20 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns>
public DataEvent<K> As<K>(K data, string? topic)
{
return new DataEvent<K>(data, topic, OriginalData, Timestamp);
return new DataEvent<K>(data, topic, OriginalData, Timestamp, UpdateType);
}
/// <summary>
/// Create a new DataEvent with data in the from of type K based on the current DataEvent. OriginalData and Timestamp will be copied over
/// </summary>
/// <typeparam name="K">The type of the new data</typeparam>
/// <param name="data">The new data</param>
/// <param name="topic">The new topic</param>
/// <param name="updateType">The type of update</param>
/// <returns></returns>
public DataEvent<K> As<K>(K data, string? topic, SocketUpdateType updateType)
{
return new DataEvent<K>(data, topic, OriginalData, Timestamp, updateType);
}
}
}
@@ -1,24 +1,24 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using System;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Sockets
namespace CryptoExchange.Net.Objects.Sockets
{
/// <summary>
/// Subscription to a data stream
/// </summary>
public class UpdateSubscription
{
private readonly SocketConnection connection;
private readonly SocketSubscription subscription;
private readonly SocketConnection _connection;
private readonly Subscription _listener;
/// <summary>
/// Event when the connection is lost. The socket will automatically reconnect when possible.
/// </summary>
public event Action ConnectionLost
{
add => connection.ConnectionLost += value;
remove => connection.ConnectionLost -= value;
add => _connection.ConnectionLost += value;
remove => _connection.ConnectionLost -= value;
}
/// <summary>
@@ -26,8 +26,8 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public event Action ConnectionClosed
{
add => connection.ConnectionClosed += value;
remove => connection.ConnectionClosed -= value;
add => _connection.ConnectionClosed += value;
remove => _connection.ConnectionClosed -= value;
}
/// <summary>
@@ -37,8 +37,8 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public event Action<TimeSpan> ConnectionRestored
{
add => connection.ConnectionRestored += value;
remove => connection.ConnectionRestored -= value;
add => _connection.ConnectionRestored += value;
remove => _connection.ConnectionRestored -= value;
}
/// <summary>
@@ -46,8 +46,8 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public event Action ActivityPaused
{
add => connection.ActivityPaused += value;
remove => connection.ActivityPaused -= value;
add => _connection.ActivityPaused += value;
remove => _connection.ActivityPaused -= value;
}
/// <summary>
@@ -55,8 +55,8 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public event Action ActivityUnpaused
{
add => connection.ActivityUnpaused += value;
remove => connection.ActivityUnpaused -= value;
add => _connection.ActivityUnpaused += value;
remove => _connection.ActivityUnpaused -= value;
}
/// <summary>
@@ -64,38 +64,38 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public event Action<Exception> Exception
{
add => subscription.Exception += value;
remove => subscription.Exception -= value;
add => _listener.Exception += value;
remove => _listener.Exception -= value;
}
/// <summary>
/// The id of the socket
/// </summary>
public int SocketId => connection.SocketId;
public int SocketId => _connection.SocketId;
/// <summary>
/// The id of the subscription
/// </summary>
public int Id => subscription.Id;
public int Id => _listener.Id;
/// <summary>
/// ctor
/// </summary>
/// <param name="connection">The socket connection the subscription is on</param>
/// <param name="subscription">The subscription</param>
public UpdateSubscription(SocketConnection connection, SocketSubscription subscription)
public UpdateSubscription(SocketConnection connection, Subscription subscription)
{
this.connection = connection;
this.subscription = subscription;
_connection = connection;
_listener = subscription;
}
/// <summary>
/// Close the subscription
/// </summary>
/// <returns></returns>
public Task CloseAsync()
{
return connection.CloseAsync(subscription);
return _connection.CloseAsync(_listener);
}
/// <summary>
@@ -104,7 +104,7 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns>
public Task ReconnectAsync()
{
return connection.TriggerReconnectAsync();
return _connection.TriggerReconnectAsync();
}
/// <summary>
@@ -113,16 +113,16 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns>
internal async Task UnsubscribeAsync()
{
await connection.UnsubscribeAsync(subscription).ConfigureAwait(false);
await _connection.UnsubscribeAsync(_listener).ConfigureAwait(false);
}
/// <summary>
/// Resubscribe this subscription
/// </summary>
/// <returns></returns>
internal async Task<CallResult<bool>> ResubscribeAsync()
internal async Task<CallResult> ResubscribeAsync()
{
return await connection.ResubscribeAsync(subscription).ConfigureAwait(false);
return await _connection.ResubscribeAsync(_listener).ConfigureAwait(false);
}
}
}
@@ -1,10 +1,9 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Sockets
namespace CryptoExchange.Net.Objects.Sockets
{
/// <summary>
/// Parameters for a websocket
@@ -15,51 +14,46 @@ namespace CryptoExchange.Net.Sockets
/// The uri to connect to
/// </summary>
public Uri Uri { get; set; }
/// <summary>
/// Headers to send in the connection handshake
/// </summary>
public IDictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
/// <summary>
/// Cookies to send in the connection handshake
/// </summary>
public IDictionary<string, string> Cookies { get; set; } = new Dictionary<string, string>();
/// <summary>
/// The time to wait between reconnect attempts
/// </summary>
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
/// <summary>
/// Proxy for the connection
/// </summary>
public ApiProxy? Proxy { get; set; }
/// <summary>
/// Whether the socket should automatically reconnect when connection is lost
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// The maximum time of no data received before considering the connection lost and closting/reconnecting the socket
/// </summary>
public TimeSpan? Timeout { get; set; }
/// <summary>
/// Interval at which to send ping frames
/// </summary>
public TimeSpan? KeepAliveInterval { get; set; }
/// <summary>
/// The max amount of messages to send per second
/// </summary>
public int? RatelimitPerSecond { get; set; }
/// <summary>
/// Origin header value to send in the connection handshake
/// </summary>
public string? Origin { get; set; }
/// <summary>
/// Delegate used for processing byte data received from socket connections before it is processed by handlers
/// </summary>
public Func<byte[], string>? DataInterpreterBytes { get; set; }
/// <summary>
/// Delegate used for processing string data received from socket connections before it is processed by handlers
/// The rate limiters for the socket connection
/// </summary>
public Func<string, string>? DataInterpreterString { get; set; }
public IEnumerable<IRateLimiter>? RateLimiters { get; set; }
/// <summary>
/// Encoding for sending/receiving data
+6 -7
View File
@@ -1,6 +1,5 @@
using System;
using System.Threading;
using CryptoExchange.Net.Logging;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Objects
@@ -45,7 +44,7 @@ namespace CryptoExchange.Net.Objects
/// <summary>
/// Logger
/// </summary>
public Log Log { get; }
public ILogger Logger { get; }
/// <summary>
/// Should synchronize time
/// </summary>
@@ -62,13 +61,13 @@ namespace CryptoExchange.Net.Objects
/// <summary>
/// ctor
/// </summary>
/// <param name="log"></param>
/// <param name="logger"></param>
/// <param name="recalculationInterval"></param>
/// <param name="syncTime"></param>
/// <param name="syncState"></param>
public TimeSyncInfo(Log log, bool syncTime, TimeSpan recalculationInterval, TimeSyncState syncState)
public TimeSyncInfo(ILogger logger, bool syncTime, TimeSpan recalculationInterval, TimeSyncState syncState)
{
Log = log;
Logger = logger;
SyncTime = syncTime;
RecalculationInterval = recalculationInterval;
TimeSyncState = syncState;
@@ -83,12 +82,12 @@ namespace CryptoExchange.Net.Objects
TimeSyncState.LastSyncTime = DateTime.UtcNow;
if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500)
{
Log.Write(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset within limits, set offset to 0ms");
Logger.Log(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset within limits, set offset to 0ms");
TimeSyncState.TimeOffset = TimeSpan.Zero;
}
else
{
Log.Write(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset set to {Math.Round(offset.TotalMilliseconds)}ms");
Logger.Log(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset set to {Math.Round(offset.TotalMilliseconds)}ms");
TimeSyncState.TimeOffset = offset;
}
}
+63
View File
@@ -0,0 +1,63 @@
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Trace logger provider for creating trace loggers
/// </summary>
public class TraceLoggerProvider : ILoggerProvider
{
private readonly LogLevel _logLevel;
/// <summary>
/// ctor
/// </summary>
/// <param name="logLevel"></param>
public TraceLoggerProvider(LogLevel? logLevel = null)
{
_logLevel = logLevel ?? LogLevel.Trace;
}
/// <inheritdoc />
public ILogger CreateLogger(string categoryName) => new TraceLogger(categoryName, _logLevel);
/// <inheritdoc />
public void Dispose() { }
}
/// <summary>
/// Trace logger
/// </summary>
public class TraceLogger : ILogger
{
private string? _categoryName;
private LogLevel _logLevel;
/// <summary>
/// ctor
/// </summary>
/// <param name="categoryName"></param>
/// <param name="level"></param>
public TraceLogger(string? categoryName = null, LogLevel level = LogLevel.Trace)
{
_categoryName = categoryName;
_logLevel = level;
}
/// <inheritdoc />
public IDisposable BeginScope<TState>(TState state) => null!;
/// <inheritdoc />
public bool IsEnabled(LogLevel logLevel) => (int)logLevel < (int)_logLevel;
/// <inheritdoc />
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if ((int)logLevel < (int)_logLevel)
return;
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {(_categoryName == null ? "" : $"{_categoryName} | ")}{formatter(state, exception)}";
Trace.WriteLine(logMessage);
}
}
}
@@ -0,0 +1,37 @@
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Trade environment names
/// </summary>
public static class TradeEnvironmentNames
{
/// <summary>
/// Live environment
/// </summary>
public const string Live = "live";
/// <summary>
/// Testnet environment
/// </summary>
public const string Testnet = "testnet";
}
/// <summary>
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
/// the echange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
/// </summary>
public class TradeEnvironment
{
/// <summary>
/// Name of the environment
/// </summary>
public string EnvironmentName { get; init; }
/// <summary>
/// </summary>
/// <param name="name"></param>
protected TradeEnvironment(string name)
{
EnvironmentName = name;
}
}
}
@@ -13,14 +13,17 @@ namespace CryptoExchange.Net.OrderBook
/// First sequence number in this update
/// </summary>
public long FirstUpdateId { get; set; }
/// <summary>
/// Last sequence number in this update
/// </summary>
public long LastUpdateId { get; set; }
/// <summary>
/// List of changed/new asks
/// </summary>
public IEnumerable<ISymbolOrderBookEntry> Asks { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
/// <summary>
/// List of changed/new bids
/// </summary>
+82 -76
View File
@@ -7,9 +7,9 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.OrderBook
@@ -30,7 +30,7 @@ namespace CryptoExchange.Net.OrderBook
private readonly AsyncResetEvent _queueEvent;
private readonly ConcurrentQueue<object> _processQueue;
private readonly bool _validateChecksum;
private bool _validateChecksum;
private class EmptySymbolOrderBookEntry : ISymbolOrderBookEntry
{
@@ -42,46 +42,45 @@ namespace CryptoExchange.Net.OrderBook
private static readonly ISymbolOrderBookEntry _emptySymbolOrderBookEntry = new EmptySymbolOrderBookEntry();
/// <summary>
/// A buffer to store messages received before the initial book snapshot is processed. These messages
/// will be processed after the book snapshot is set. Any messages in this buffer with sequence numbers lower
/// than the snapshot sequence number will be discarded
/// </summary>
protected readonly List<ProcessBufferRangeSequenceEntry> processBuffer;
protected readonly List<ProcessBufferRangeSequenceEntry> _processBuffer;
/// <summary>
/// The ask list, should only be accessed using the bookLock
/// </summary>
protected SortedList<decimal, ISymbolOrderBookEntry> asks;
protected SortedList<decimal, ISymbolOrderBookEntry> _asks;
/// <summary>
/// The bid list, should only be accessed using the bookLock
/// </summary>
protected SortedList<decimal, ISymbolOrderBookEntry> bids;
protected SortedList<decimal, ISymbolOrderBookEntry> _bids;
/// <summary>
/// The log
/// </summary>
protected Log log;
protected ILogger _logger;
/// <summary>
/// Whether update numbers are consecutive. If set to true and an update comes in which isn't the previous sequences number + 1
/// the book will resynchronize as it is deemed out of sync
/// </summary>
protected bool sequencesAreConsecutive;
protected bool _sequencesAreConsecutive;
/// <summary>
/// Whether levels should be strictly enforced. For example, when an order book has 25 levels and a new update comes in which pushes
/// the current level 25 ask out of the top 25, should the curent the level 26 entry be removed from the book or does the
/// server handle this
/// </summary>
protected bool strictLevels;
protected bool _strictLevels;
/// <summary>
/// If the initial snapshot of the book has been set
/// </summary>
protected bool bookSet;
protected bool _bookSet;
/// <summary>
/// The amount of levels for this book
@@ -102,7 +101,7 @@ namespace CryptoExchange.Net.OrderBook
var old = _status;
_status = value;
log.Write(LogLevel.Information, $"{Id} order book {Symbol} status changed: {old} => {value}");
_logger.Log(LogLevel.Information, $"{Id} order book {Symbol} status changed: {old} => {value}");
OnStatusChange?.Invoke(old, _status);
}
}
@@ -137,7 +136,7 @@ namespace CryptoExchange.Net.OrderBook
get
{
lock (_bookLock)
return asks.Select(a => a.Value).ToList();
return _asks.Select(a => a.Value).ToList();
}
}
@@ -147,7 +146,7 @@ namespace CryptoExchange.Net.OrderBook
get
{
lock (_bookLock)
return bids.Select(a => a.Value).ToList();
return _bids.Select(a => a.Value).ToList();
}
}
@@ -167,7 +166,7 @@ namespace CryptoExchange.Net.OrderBook
get
{
lock (_bookLock)
return bids.FirstOrDefault().Value ?? _emptySymbolOrderBookEntry;
return _bids.FirstOrDefault().Value ?? _emptySymbolOrderBookEntry;
}
}
@@ -177,7 +176,7 @@ namespace CryptoExchange.Net.OrderBook
get
{
lock (_bookLock)
return asks.FirstOrDefault().Value ?? _emptySymbolOrderBookEntry;
return _asks.FirstOrDefault().Value ?? _emptySymbolOrderBookEntry;
}
}
@@ -192,32 +191,39 @@ namespace CryptoExchange.Net.OrderBook
/// <summary>
/// ctor
/// </summary>
/// <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="symbol">The symbol the order book is for</param>
/// <param name="options">The options for the order book</param>
protected SymbolOrderBook(string id, string symbol, OrderBookOptions options)
protected SymbolOrderBook(ILogger? logger, string id, string symbol)
{
if (symbol == null)
throw new ArgumentNullException(nameof(symbol));
if (options == null)
throw new ArgumentNullException(nameof(options));
Id = id;
processBuffer = new List<ProcessBufferRangeSequenceEntry>();
_processBuffer = new List<ProcessBufferRangeSequenceEntry>();
_processQueue = new ConcurrentQueue<object>();
_queueEvent = new AsyncResetEvent(false, true);
_validateChecksum = options.ChecksumValidationEnabled;
Symbol = symbol;
Status = OrderBookStatus.Disconnected;
asks = new SortedList<decimal, ISymbolOrderBookEntry>();
bids = new SortedList<decimal, ISymbolOrderBookEntry>(new DescComparer<decimal>());
_asks = new SortedList<decimal, ISymbolOrderBookEntry>();
_bids = new SortedList<decimal, ISymbolOrderBookEntry>(new DescComparer<decimal>());
log = new Log(id) { Level = options.LogLevel };
var writers = options.LogWriters ?? new List<ILogger> { new DebugLogger() };
log.UpdateWriters(writers.ToList());
_logger = logger ?? new TraceLogger();
}
/// <summary>
/// Initialize the order book using the provided options
/// </summary>
/// <param name="options">The options</param>
/// <exception cref="ArgumentNullException"></exception>
protected void Initialize(OrderBookOptions options)
{
if (options == null)
throw new ArgumentNullException(nameof(options));
_validateChecksum = options.ChecksumValidationEnabled;
}
/// <inheritdoc/>
@@ -226,7 +232,7 @@ namespace CryptoExchange.Net.OrderBook
if (Status != OrderBookStatus.Disconnected)
throw new InvalidOperationException($"Can't start book unless state is {OrderBookStatus.Disconnected}. Current state: {Status}");
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} starting");
_logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} starting");
_cts = new CancellationTokenSource();
ct?.Register(async () =>
{
@@ -236,8 +242,8 @@ namespace CryptoExchange.Net.OrderBook
// Clear any previous messages
while (_processQueue.TryDequeue(out _)) { }
processBuffer.Clear();
bookSet = false;
_processBuffer.Clear();
_bookSet = false;
Status = OrderBookStatus.Connecting;
_processTask = Task.Factory.StartNew(ProcessQueue, TaskCreationOptions.LongRunning);
@@ -251,7 +257,7 @@ namespace CryptoExchange.Net.OrderBook
if (_cts.IsCancellationRequested)
{
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} stopped while starting");
_logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} stopped while starting");
await startResult.Data.CloseAsync().ConfigureAwait(false);
Status = OrderBookStatus.Disconnected;
return new CallResult<bool>(new CancellationRequestedError());
@@ -267,7 +273,7 @@ namespace CryptoExchange.Net.OrderBook
}
private void HandleConnectionLost() {
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} connection lost");
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} connection lost");
if (Status != OrderBookStatus.Disposed) {
Status = OrderBookStatus.Reconnecting;
Reset();
@@ -275,7 +281,7 @@ namespace CryptoExchange.Net.OrderBook
}
private void HandleConnectionClosed() {
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} disconnected");
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} disconnected");
Status = OrderBookStatus.Disconnected;
_ = StopAsync();
}
@@ -287,7 +293,7 @@ namespace CryptoExchange.Net.OrderBook
/// <inheritdoc/>
public async Task StopAsync()
{
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} stopping");
_logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} stopping");
Status = OrderBookStatus.Disconnected;
_cts?.Cancel();
_queueEvent.Set();
@@ -300,7 +306,7 @@ namespace CryptoExchange.Net.OrderBook
_subscription.ConnectionClosed -= HandleConnectionClosed;
_subscription.ConnectionRestored -= HandleConnectionRestored;
}
log.Write(LogLevel.Trace, $"{Id} order book {Symbol} stopped");
_logger.Log(LogLevel.Trace, $"{Id} order book {Symbol} stopped");
}
/// <inheritdoc/>
@@ -314,7 +320,7 @@ namespace CryptoExchange.Net.OrderBook
var amountLeft = baseQuantity;
lock (_bookLock)
{
var list = type == OrderBookEntryType.Ask ? asks : bids;
var list = type == OrderBookEntryType.Ask ? _asks : _bids;
var step = 0;
while (amountLeft > 0)
@@ -344,7 +350,7 @@ namespace CryptoExchange.Net.OrderBook
var totalBaseQuantity = 0m;
lock (_bookLock)
{
var list = type == OrderBookEntryType.Ask ? asks : bids;
var list = type == OrderBookEntryType.Ask ? _asks : _bids;
var step = 0;
while (quoteQuantityLeft > 0)
@@ -456,14 +462,14 @@ namespace CryptoExchange.Net.OrderBook
/// </summary>
protected void CheckProcessBuffer()
{
var pbList = processBuffer.ToList();
var pbList = _processBuffer.ToList();
if (pbList.Count > 0)
log.Write(LogLevel.Debug, $"Processing {pbList.Count} buffered updates");
_logger.Log(LogLevel.Debug, $"{Id} Processing {pbList.Count} buffered updates");
foreach (var bufferEntry in pbList)
{
ProcessRangeUpdates(bufferEntry.FirstUpdateId, bufferEntry.LastUpdateId, bufferEntry.Bids, bufferEntry.Asks);
processBuffer.Remove(bufferEntry);
_processBuffer.Remove(bufferEntry);
}
}
@@ -477,21 +483,21 @@ namespace CryptoExchange.Net.OrderBook
{
if (sequence <= LastSequenceNumber)
{
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} update skipped #{sequence}, currently at #{LastSequenceNumber}");
_logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} update skipped #{sequence}, currently at #{LastSequenceNumber}");
return false;
}
if (sequencesAreConsecutive && sequence > LastSequenceNumber + 1)
if (_sequencesAreConsecutive && sequence > LastSequenceNumber + 1)
{
// Out of sync
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} out of sync (expected { LastSequenceNumber + 1}, was {sequence}), reconnecting");
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} out of sync (expected { LastSequenceNumber + 1}, was {sequence}), reconnecting");
_stopProcessing = true;
Resubscribe();
return false;
}
UpdateTime = DateTime.UtcNow;
var listToChange = type == OrderBookEntryType.Ask ? asks : bids;
var listToChange = type == OrderBookEntryType.Ask ? _asks : _bids;
if (entry.Quantity == 0)
{
if (!listToChange.ContainsKey(entry.Price))
@@ -527,7 +533,7 @@ namespace CryptoExchange.Net.OrderBook
protected async Task<CallResult<bool>> WaitForSetOrderBookAsync(TimeSpan timeout, CancellationToken ct)
{
var startWait = DateTime.UtcNow;
while (!bookSet && Status == OrderBookStatus.Syncing)
while (!_bookSet && Status == OrderBookStatus.Syncing)
{
if(ct.IsCancellationRequested)
return new CallResult<bool>(new CancellationRequestedError());
@@ -569,9 +575,9 @@ namespace CryptoExchange.Net.OrderBook
// Clear queue
while (_processQueue.TryDequeue(out _)) { }
processBuffer.Clear();
asks.Clear();
bids.Clear();
_processBuffer.Clear();
_asks.Clear();
_bids.Clear();
AskCount = 0;
BidCount = 0;
@@ -620,8 +626,8 @@ namespace CryptoExchange.Net.OrderBook
_queueEvent.Set();
// Clear queue
while (_processQueue.TryDequeue(out _)) { }
processBuffer.Clear();
bookSet = false;
_processBuffer.Clear();
_bookSet = false;
DoReset();
}
@@ -638,7 +644,7 @@ namespace CryptoExchange.Net.OrderBook
success = resyncResult;
}
log.Write(LogLevel.Information, $"{Id} order book {Symbol} successfully resynchronized");
_logger.Log(LogLevel.Information, $"{Id} order book {Symbol} successfully resynchronized");
Status = OrderBookStatus.Synced;
}
@@ -655,7 +661,7 @@ namespace CryptoExchange.Net.OrderBook
if (_stopProcessing)
{
log.Write(LogLevel.Trace, "Skipping message because of resubscribing");
_logger.Log(LogLevel.Trace, $"{Id} Skipping message because of resubscribing");
continue;
}
@@ -673,21 +679,21 @@ namespace CryptoExchange.Net.OrderBook
{
lock (_bookLock)
{
bookSet = true;
asks.Clear();
_bookSet = true;
_asks.Clear();
foreach (var ask in item.Asks)
asks.Add(ask.Price, ask);
bids.Clear();
_asks.Add(ask.Price, ask);
_bids.Clear();
foreach (var bid in item.Bids)
bids.Add(bid.Price, bid);
_bids.Add(bid.Price, bid);
LastSequenceNumber = item.EndUpdateId;
AskCount = asks.Count;
BidCount = bids.Count;
AskCount = _asks.Count;
BidCount = _bids.Count;
UpdateTime = DateTime.UtcNow;
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks. #{item.EndUpdateId}");
_logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks. #{item.EndUpdateId}");
CheckProcessBuffer();
OnOrderBookUpdate?.Invoke((item.Bids, item.Asks));
OnBestOffersChanged?.Invoke((BestBid, BestAsk));
@@ -698,16 +704,16 @@ namespace CryptoExchange.Net.OrderBook
{
lock (_bookLock)
{
if (!bookSet)
if (!_bookSet)
{
processBuffer.Add(new ProcessBufferRangeSequenceEntry()
_processBuffer.Add(new ProcessBufferRangeSequenceEntry()
{
Asks = item.Asks,
Bids = item.Bids,
FirstUpdateId = item.StartUpdateId,
LastUpdateId = item.EndUpdateId,
});
log.Write(LogLevel.Trace, $"{Id} order book {Symbol} update buffered #{item.StartUpdateId}-#{item.EndUpdateId} [{item.Asks.Count()} asks, {item.Bids.Count()} bids]");
_logger.Log(LogLevel.Trace, $"{Id} order book {Symbol} update buffered #{item.StartUpdateId}-#{item.EndUpdateId} [{item.Asks.Count()} asks, {item.Bids.Count()} bids]");
}
else
{
@@ -715,12 +721,12 @@ namespace CryptoExchange.Net.OrderBook
var (prevBestBid, prevBestAsk) = BestOffers;
ProcessRangeUpdates(item.StartUpdateId, item.EndUpdateId, item.Bids, item.Asks);
if (!asks.Any() || !bids.Any())
if (!_asks.Any() || !_bids.Any())
return;
if (asks.First().Key < bids.First().Key)
if (_asks.First().Key < _bids.First().Key)
{
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} detected out of sync order book. First ask: {asks.First().Key}, first bid: {bids.First().Key}. Resyncing");
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} detected out of sync order book. First ask: {_asks.First().Key}, first bid: {_bids.First().Key}. Resyncing");
_stopProcessing = true;
Resubscribe();
return;
@@ -754,7 +760,7 @@ namespace CryptoExchange.Net.OrderBook
if (!checksumResult)
{
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} out of sync. Resyncing");
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} out of sync. Resyncing");
_stopProcessing = true;
Resubscribe();
}
@@ -778,7 +784,7 @@ namespace CryptoExchange.Net.OrderBook
if (!await _subscription!.ResubscribeAsync().ConfigureAwait(false))
{
// Resubscribing failed, reconnect the socket
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} resync failed, reconnecting socket");
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} resync failed, reconnecting socket");
Status = OrderBookStatus.Reconnecting;
_ = _subscription!.ReconnectAsync();
}
@@ -793,7 +799,7 @@ namespace CryptoExchange.Net.OrderBook
{
if (lastUpdateId <= LastSequenceNumber)
{
log.Write(LogLevel.Trace, $"{Id} order book {Symbol} update skipped #{firstUpdateId}-{lastUpdateId}");
_logger.Log(LogLevel.Trace, $"{Id} order book {Symbol} update skipped #{firstUpdateId}-{lastUpdateId}");
return;
}
@@ -803,23 +809,23 @@ namespace CryptoExchange.Net.OrderBook
foreach (var entry in asks)
ProcessUpdate(LastSequenceNumber + 1, OrderBookEntryType.Ask, entry);
if (Levels.HasValue && strictLevels)
if (Levels.HasValue && _strictLevels)
{
while (this.bids.Count > Levels.Value)
while (this._bids.Count > Levels.Value)
{
BidCount--;
this.bids.Remove(this.bids.Last().Key);
this._bids.Remove(this._bids.Last().Key);
}
while (this.asks.Count > Levels.Value)
while (this._asks.Count > Levels.Value)
{
AskCount--;
this.asks.Remove(this.asks.Last().Key);
this._asks.Remove(this._asks.Last().Key);
}
}
LastSequenceNumber = lastUpdateId;
log.Write(LogLevel.Trace, $"{Id} order book {Symbol} update processed #{firstUpdateId}-{lastUpdateId}");
_logger.Log(LogLevel.Trace, $"{Id} order book {Symbol} update processed #{firstUpdateId}-{lastUpdateId}");
}
}
+13 -13
View File
@@ -15,8 +15,8 @@ namespace CryptoExchange.Net.Requests
/// </summary>
public class Request : IRequest
{
private readonly HttpRequestMessage request;
private readonly HttpClient httpClient;
private readonly HttpRequestMessage _request;
private readonly HttpClient _httpClient;
/// <summary>
/// Create request object for web request
@@ -26,8 +26,8 @@ namespace CryptoExchange.Net.Requests
/// <param name="requestId"></param>
public Request(HttpRequestMessage request, HttpClient client, int requestId)
{
httpClient = client;
this.request = request;
_httpClient = client;
_request = request;
RequestId = requestId;
}
@@ -37,18 +37,18 @@ namespace CryptoExchange.Net.Requests
/// <inheritdoc />
public string Accept
{
set => request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(value));
set => _request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(value));
}
/// <inheritdoc />
public HttpMethod Method
{
get => request.Method;
set => request.Method = value;
get => _request.Method;
set => _request.Method = value;
}
/// <inheritdoc />
public Uri Uri => request.RequestUri;
public Uri Uri => _request.RequestUri;
/// <inheritdoc />
public int RequestId { get; }
@@ -57,31 +57,31 @@ namespace CryptoExchange.Net.Requests
public void SetContent(string data, string contentType)
{
Content = data;
request.Content = new StringContent(data, Encoding.UTF8, contentType);
_request.Content = new StringContent(data, Encoding.UTF8, contentType);
}
/// <inheritdoc />
public void AddHeader(string key, string value)
{
request.Headers.Add(key, value);
_request.Headers.Add(key, value);
}
/// <inheritdoc />
public Dictionary<string, IEnumerable<string>> GetHeaders()
{
return request.Headers.ToDictionary(h => h.Key, h => h.Value);
return _request.Headers.ToDictionary(h => h.Key, h => h.Value);
}
/// <inheritdoc />
public void SetContent(byte[] data)
{
request.Content = new ByteArrayContent(data);
_request.Content = new ByteArrayContent(data);
}
/// <inheritdoc />
public async Task<IResponse> GetResponseAsync(CancellationToken cancellationToken)
{
return new Response(await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false));
return new Response(await _httpClient.SendAsync(_request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false));
}
}
}
+15 -13
View File
@@ -11,37 +11,39 @@ namespace CryptoExchange.Net.Requests
/// </summary>
public class RequestFactory : IRequestFactory
{
private HttpClient? httpClient;
private HttpClient? _httpClient;
/// <inheritdoc />
public void Configure(TimeSpan requestTimeout, ApiProxy? proxy, HttpClient? client = null)
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null)
{
if (client == null)
{
HttpMessageHandler handler = new HttpClientHandler()
var handler = new HttpClientHandler();
if (proxy != null)
{
Proxy = proxy == null ? null : new WebProxy
handler.Proxy = new WebProxy
{
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
}
};
};
}
httpClient = new HttpClient(handler) { Timeout = requestTimeout };
}
else
{
httpClient = client;
client = new HttpClient(handler)
{
Timeout = requestTimeout
};
}
_httpClient = client;
}
/// <inheritdoc />
public IRequest Create(HttpMethod method, Uri uri, int requestId)
{
if (httpClient == null)
if (_httpClient == null)
throw new InvalidOperationException("Cant create request before configuring http client");
return new Request(new HttpRequestMessage(method, uri), httpClient, requestId);
return new Request(new HttpRequestMessage(method, uri), _httpClient, requestId);
}
}
}
+10 -7
View File
@@ -12,16 +12,19 @@ namespace CryptoExchange.Net.Requests
/// </summary>
internal class Response : IResponse
{
private readonly HttpResponseMessage response;
private readonly HttpResponseMessage _response;
/// <inheritdoc />
public HttpStatusCode StatusCode => response.StatusCode;
public HttpStatusCode StatusCode => _response.StatusCode;
/// <inheritdoc />
public bool IsSuccessStatusCode => response.IsSuccessStatusCode;
public bool IsSuccessStatusCode => _response.IsSuccessStatusCode;
/// <inheritdoc />
public IEnumerable<KeyValuePair<string, IEnumerable<string>>> ResponseHeaders => response.Headers;
public long? ContentLength => _response.Content.Headers.ContentLength;
/// <inheritdoc />
public IEnumerable<KeyValuePair<string, IEnumerable<string>>> ResponseHeaders => _response.Headers;
/// <summary>
/// Create response for a http response message
@@ -29,19 +32,19 @@ namespace CryptoExchange.Net.Requests
/// <param name="response">The actual response</param>
public Response(HttpResponseMessage response)
{
this.response = response;
this._response = response;
}
/// <inheritdoc />
public async Task<Stream> GetResponseStreamAsync()
{
return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
return await _response.Content.ReadAsStreamAsync().ConfigureAwait(false);
}
/// <inheritdoc />
public void Close()
{
response.Dispose();
_response.Dispose();
}
}
}
@@ -1,6 +1,6 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
@@ -8,8 +8,8 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.WebSockets;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@@ -28,13 +28,12 @@ namespace CryptoExchange.Net.Sockets
Reconnecting
}
internal static int lastStreamId;
private static readonly object streamIdLock = new();
internal static int _lastStreamId;
private static readonly object _streamIdLock = new();
private readonly AsyncResetEvent _sendEvent;
private readonly ConcurrentQueue<byte[]> _sendBuffer;
private readonly ConcurrentQueue<SendItem> _sendBuffer;
private readonly SemaphoreSlim _closeSem;
private readonly List<DateTime> _outgoingMessages;
private ClientWebSocket _socket;
private CancellationTokenSource _ctsSource;
@@ -46,7 +45,6 @@ namespace CryptoExchange.Net.Sockets
private ProcessState _processState;
private DateTime _lastReconnectTime;
/// <summary>
/// Received messages, the size and the timstamp
/// </summary>
@@ -60,7 +58,7 @@ namespace CryptoExchange.Net.Sockets
/// <summary>
/// Log
/// </summary>
protected Log _log;
protected ILogger _logger;
/// <inheritdoc />
public int Id { get; }
@@ -77,10 +75,10 @@ namespace CryptoExchange.Net.Sockets
public Uri Uri => Parameters.Uri;
/// <inheritdoc />
public bool IsClosed => _socket.State == WebSocketState.Closed;
public virtual bool IsClosed => _socket.State == WebSocketState.Closed;
/// <inheritdoc />
public bool IsOpen => _socket.State == WebSocketState.Open && !_ctsSource.IsCancellationRequested;
public virtual bool IsOpen => _socket.State == WebSocketState.Open && !_ctsSource.IsCancellationRequested;
/// <inheritdoc />
public double IncomingKbps
@@ -100,35 +98,42 @@ namespace CryptoExchange.Net.Sockets
}
/// <inheritdoc />
public event Action? OnClose;
public event Func<Task>? OnClose;
/// <inheritdoc />
public event Action<string>? OnMessage;
public event Func<WebSocketMessageType, Stream, Task>? OnStreamMessage;
/// <inheritdoc />
public event Action<Exception>? OnError;
public event Func<int, Task>? OnRequestSent;
/// <inheritdoc />
public event Action? OnOpen;
public event Func<Exception, Task>? OnError;
/// <inheritdoc />
public event Action? OnReconnecting;
public event Func<Task>? OnOpen;
/// <inheritdoc />
public event Action? OnReconnected;
public event Func<Task>? OnReconnecting;
/// <inheritdoc />
public event Func<Task>? OnReconnected;
/// <inheritdoc />
public Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="log">The log object to use</param>
/// <param name="logger">The log object to use</param>
/// <param name="websocketParameters">The parameters for this socket</param>
public CryptoExchangeWebSocketClient(Log log, WebSocketParameters websocketParameters)
public CryptoExchangeWebSocketClient(ILogger logger, WebSocketParameters websocketParameters)
{
Id = NextStreamId();
_log = log;
_logger = logger;
Parameters = websocketParameters;
_outgoingMessages = new List<DateTime>();
_receivedMessages = new List<ReceiveItem>();
_sendEvent = new AsyncResetEvent();
_sendBuffer = new ConcurrentQueue<byte[]>();
_sendBuffer = new ConcurrentQueue<SendItem>();
_ctsSource = new CancellationTokenSource();
_receivedMessagesLock = new object();
@@ -142,7 +147,7 @@ namespace CryptoExchange.Net.Sockets
if (!await ConnectInternalAsync().ConfigureAwait(false))
return false;
OnOpen?.Invoke();
await (OnOpen?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
_processTask = ProcessAsync();
return true;
}
@@ -178,7 +183,7 @@ namespace CryptoExchange.Net.Sockets
private async Task<bool> ConnectInternalAsync()
{
_log.Write(LogLevel.Debug, $"Socket {Id} connecting");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] connecting");
try
{
using CancellationTokenSource tcs = new(TimeSpan.FromSeconds(10));
@@ -186,11 +191,11 @@ namespace CryptoExchange.Net.Sockets
}
catch (Exception e)
{
_log.Write(LogLevel.Debug, $"Socket {Id} connection failed: " + e.ToLogString());
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] connection failed: " + e.ToLogString());
return false;
}
_log.Write(LogLevel.Debug, $"Socket {Id} connected to {Uri}");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] connected to {Uri}");
return true;
}
@@ -199,13 +204,13 @@ namespace CryptoExchange.Net.Sockets
{
while (!_stopRequested)
{
_log.Write(LogLevel.Debug, $"Socket {Id} starting processing tasks");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] starting processing tasks");
_processState = ProcessState.Processing;
var sendTask = SendLoopAsync();
var receiveTask = ReceiveLoopAsync();
var timeoutTask = Parameters.Timeout != null && Parameters.Timeout > TimeSpan.FromSeconds(0) ? CheckTimeoutAsync() : Task.CompletedTask;
await Task.WhenAll(sendTask, receiveTask, timeoutTask).ConfigureAwait(false);
_log.Write(LogLevel.Debug, $"Socket {Id} processing tasks finished");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] processing tasks finished");
_processState = ProcessState.WaitingForClose;
while (_closeTask == null)
@@ -217,14 +222,14 @@ namespace CryptoExchange.Net.Sockets
if (!Parameters.AutoReconnect)
{
_processState = ProcessState.Idle;
OnClose?.Invoke();
await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
return;
}
if (!_stopRequested)
{
_processState = ProcessState.Reconnecting;
OnReconnecting?.Invoke();
await (OnReconnecting?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
}
var sinceLastReconnect = DateTime.UtcNow - _lastReconnectTime;
@@ -233,14 +238,14 @@ namespace CryptoExchange.Net.Sockets
while (!_stopRequested)
{
_log.Write(LogLevel.Debug, $"Socket {Id} attempting to reconnect");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] attempting to reconnect");
var task = GetReconnectionUrl?.Invoke();
if (task != null)
{
var reconnectUri = await task.ConfigureAwait(false);
if (reconnectUri != null && Parameters.Uri != reconnectUri)
{
_log.Write(LogLevel.Debug, $"Socket {Id} reconnect URI set to {reconnectUri}");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] reconnect URI set to {reconnectUri}");
Parameters.Uri = reconnectUri;
}
}
@@ -258,7 +263,7 @@ namespace CryptoExchange.Net.Sockets
}
_lastReconnectTime = DateTime.UtcNow;
OnReconnected?.Invoke();
await (OnReconnected?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
break;
}
}
@@ -267,14 +272,14 @@ namespace CryptoExchange.Net.Sockets
}
/// <inheritdoc />
public virtual void Send(string data)
public virtual void Send(int id, string data, int weight)
{
if (_ctsSource.IsCancellationRequested)
return;
var bytes = Parameters.Encoding.GetBytes(data);
_log.Write(LogLevel.Trace, $"Socket {Id} Adding {bytes.Length} to sent buffer");
_sendBuffer.Enqueue(bytes);
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] msg {id} - Adding {bytes.Length} bytes to send buffer");
_sendBuffer.Enqueue(new SendItem { Id = id, Weight = weight, Bytes = bytes });
_sendEvent.Set();
}
@@ -284,7 +289,7 @@ namespace CryptoExchange.Net.Sockets
if (_processState != ProcessState.Processing && IsOpen)
return;
_log.Write(LogLevel.Debug, $"Socket {Id} reconnect requested");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] reconnect requested");
_closeTask = CloseInternalAsync();
await _closeTask.ConfigureAwait(false);
}
@@ -299,18 +304,18 @@ namespace CryptoExchange.Net.Sockets
{
if (_closeTask?.IsCompleted == false)
{
_log.Write(LogLevel.Debug, $"Socket {Id} CloseAsync() waiting for existing close task");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] CloseAsync() waiting for existing close task");
await _closeTask.ConfigureAwait(false);
return;
}
if (!IsOpen)
{
_log.Write(LogLevel.Debug, $"Socket {Id} CloseAsync() socket not open");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] CloseAsync() socket not open");
return;
}
_log.Write(LogLevel.Debug, $"Socket {Id} closing");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] closing");
_closeTask = CloseInternalAsync();
}
finally
@@ -321,8 +326,8 @@ namespace CryptoExchange.Net.Sockets
await _closeTask.ConfigureAwait(false);
if(_processTask != null)
await _processTask.ConfigureAwait(false);
OnClose?.Invoke();
_log.Write(LogLevel.Debug, $"Socket {Id} closed");
await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] closed");
}
/// <summary>
@@ -374,11 +379,11 @@ namespace CryptoExchange.Net.Sockets
if (_disposed)
return;
_log.Write(LogLevel.Debug, $"Socket {Id} disposing");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] disposing");
_disposed = true;
_socket.Dispose();
_ctsSource.Dispose();
_log.Write(LogLevel.Trace, $"Socket {Id} disposed");
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] disposed");
}
/// <summary>
@@ -389,6 +394,7 @@ namespace CryptoExchange.Net.Sockets
{
try
{
var limitKey = Uri.ToString() + "/" + Id.ToString();
while (true)
{
if (_ctsSource.IsCancellationRequested)
@@ -401,25 +407,24 @@ namespace CryptoExchange.Net.Sockets
while (_sendBuffer.TryDequeue(out var data))
{
if (Parameters.RatelimitPerSecond != null)
if (Parameters.RateLimiters != null)
{
// Wait for rate limit
DateTime? start = null;
while (MessagesSentLastSecond() >= Parameters.RatelimitPerSecond)
foreach(var ratelimiter in Parameters.RateLimiters)
{
start ??= DateTime.UtcNow;
await Task.Delay(50).ConfigureAwait(false);
var limitResult = await ratelimiter.LimitRequestAsync(_logger, limitKey, HttpMethod.Get, false, null, RateLimitingBehaviour.Wait, data.Weight, _ctsSource.Token).ConfigureAwait(false);
if (limitResult.Success)
{
if (limitResult.Data > 0)
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] msg {data.Id} - send delayed {limitResult.Data}ms because of rate limit");
}
}
if (start != null)
_log.Write(LogLevel.Debug, $"Socket {Id} sent delayed {Math.Round((DateTime.UtcNow - start.Value).TotalMilliseconds)}ms because of rate limit");
}
try
{
await _socket.SendAsync(new ArraySegment<byte>(data, 0, data.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false);
_outgoingMessages.Add(DateTime.UtcNow);
_log.Write(LogLevel.Trace, $"Socket {Id} sent {data.Length} bytes");
await _socket.SendAsync(new ArraySegment<byte>(data.Bytes, 0, data.Bytes.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false);
await (OnRequestSent?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false);
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] msg {data.Id} - sent {data.Bytes.Length} bytes");
}
catch (OperationCanceledException)
{
@@ -429,7 +434,7 @@ namespace CryptoExchange.Net.Sockets
catch (Exception ioe)
{
// Connection closed unexpectedly, .NET framework
OnError?.Invoke(ioe);
await (OnError?.Invoke(ioe) ?? Task.CompletedTask).ConfigureAwait(false);
if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync();
break;
@@ -442,13 +447,13 @@ namespace CryptoExchange.Net.Sockets
// Because this is running in a separate task and not awaited until the socket gets closed
// any exception here will crash the send processing, but do so silently unless the socket get's stopped.
// Make sure we at least let the owner know there was an error
_log.Write(LogLevel.Warning, $"Socket {Id} Send loop stopped with exception");
OnError?.Invoke(e);
_logger.Log(LogLevel.Warning, $"[Sckt {Id}] send loop stopped with exception");
await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false);
throw;
}
finally
{
_log.Write(LogLevel.Debug, $"Socket {Id} Send loop finished");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] send loop finished");
}
}
@@ -487,7 +492,7 @@ namespace CryptoExchange.Net.Sockets
catch (Exception wse)
{
// Connection closed unexpectedly
OnError?.Invoke(wse);
await (OnError?.Invoke(wse) ?? Task.CompletedTask).ConfigureAwait(false);
if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync();
break;
@@ -496,7 +501,7 @@ namespace CryptoExchange.Net.Sockets
if (receiveResult.MessageType == WebSocketMessageType.Close)
{
// Connection closed unexpectedly
_log.Write(LogLevel.Debug, $"Socket {Id} received `Close` message");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] received `Close` message");
if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync();
break;
@@ -507,7 +512,7 @@ namespace CryptoExchange.Net.Sockets
// We received data, but it is not complete, write it to a memory stream for reassembling
multiPartMessage = true;
memoryStream ??= new MemoryStream();
_log.Write(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in partial message");
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] received {receiveResult.Count} bytes in partial message");
await memoryStream.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false);
}
else
@@ -515,13 +520,13 @@ namespace CryptoExchange.Net.Sockets
if (!multiPartMessage)
{
// Received a complete message and it's not multi part
_log.Write(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in single message");
HandleMessage(buffer.Array!, buffer.Offset, receiveResult.Count, receiveResult.MessageType);
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] received {receiveResult.Count} bytes in single message");
await ProcessData(receiveResult.MessageType, new MemoryStream(buffer.Array, buffer.Offset, receiveResult.Count)).ConfigureAwait(false);
}
else
{
// Received the end of a multipart message, write to memory stream for reassembling
_log.Write(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in partial message");
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] received {receiveResult.Count} bytes in partial message");
await memoryStream!.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false);
}
break;
@@ -549,12 +554,14 @@ namespace CryptoExchange.Net.Sockets
if (receiveResult?.EndOfMessage == true)
{
// Reassemble complete message from memory stream
_log.Write(LogLevel.Trace, $"Socket {Id} reassembled message of {memoryStream!.Length} bytes");
HandleMessage(memoryStream!.ToArray(), 0, (int)memoryStream.Length, receiveResult.MessageType);
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] reassembled message of {memoryStream!.Length} bytes");
await ProcessData(receiveResult.MessageType, memoryStream).ConfigureAwait(false);
memoryStream.Dispose();
}
else
_log.Write(LogLevel.Trace, $"Socket {Id} discarding incomplete message of {memoryStream!.Length} bytes");
{
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] discarding incomplete message of {memoryStream!.Length} bytes");
}
}
}
}
@@ -563,113 +570,38 @@ namespace CryptoExchange.Net.Sockets
// Because this is running in a separate task and not awaited until the socket gets closed
// any exception here will crash the receive processing, but do so silently unless the socket gets stopped.
// Make sure we at least let the owner know there was an error
_log.Write(LogLevel.Warning, $"Socket {Id} Receive loop stopped with exception");
OnError?.Invoke(e);
_logger.Log(LogLevel.Warning, $"[Sckt {Id}] receive loop stopped with exception");
await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false);
throw;
}
finally
{
_log.Write(LogLevel.Debug, $"Socket {Id} Receive loop finished");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] receive loop finished");
}
}
/// <summary>
/// Handles the message
/// Proccess a stream message
/// </summary>
/// <param name="data"></param>
/// <param name="offset"></param>
/// <param name="count"></param>
/// <param name="messageType"></param>
private void HandleMessage(byte[] data, int offset, int count, WebSocketMessageType messageType)
{
string strData;
if (messageType == WebSocketMessageType.Binary)
{
if (Parameters.DataInterpreterBytes == null)
throw new Exception("Byte interpreter not set while receiving byte data");
try
{
var relevantData = new byte[count];
Array.Copy(data, offset, relevantData, 0, count);
strData = Parameters.DataInterpreterBytes(relevantData);
}
catch(Exception e)
{
_log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during byte data interpretation: " + e.ToLogString());
return;
}
}
else
strData = Parameters.Encoding.GetString(data, offset, count);
if (Parameters.DataInterpreterString != null)
{
try
{
strData = Parameters.DataInterpreterString(strData);
}
catch(Exception e)
{
_log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during string data interpretation: " + e.ToLogString());
return;
}
}
try
{
LastActionTime = DateTime.UtcNow;
OnMessage?.Invoke(strData);
}
catch(Exception e)
{
_log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during message processing: " + e.ToLogString());
}
}
/// <summary>
/// Trigger the OnMessage event
/// </summary>
/// <param name="data"></param>
protected void TriggerOnMessage(string data)
/// <param name="type"></param>
/// <param name="stream"></param>
/// <returns></returns>
protected async Task ProcessData(WebSocketMessageType type, Stream stream)
{
LastActionTime = DateTime.UtcNow;
OnMessage?.Invoke(data);
stream.Position = 0;
if (OnStreamMessage != null)
await OnStreamMessage.Invoke(type, stream).ConfigureAwait(false);
}
/// <summary>
/// Trigger the OnError event
/// </summary>
/// <param name="ex"></param>
protected void TriggerOnError(Exception ex) => OnError?.Invoke(ex);
/// <summary>
/// Trigger the OnError event
/// </summary>
protected void TriggerOnOpen() => OnOpen?.Invoke();
/// <summary>
/// Trigger the OnError event
/// </summary>
protected void TriggerOnClose() => OnClose?.Invoke();
/// <summary>
/// Trigger the OnReconnecting event
/// </summary>
protected void TriggerOnReconnecting() => OnReconnecting?.Invoke();
/// <summary>
/// Trigger the OnReconnected event
/// </summary>
protected void TriggerOnReconnected() => OnReconnected?.Invoke();
/// <summary>
/// Checks if there is no data received for a period longer than the specified timeout
/// </summary>
/// <returns></returns>
protected async Task CheckTimeoutAsync()
{
_log.Write(LogLevel.Debug, $"Socket {Id} Starting task checking for no data received for {Parameters.Timeout}");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] starting task checking for no data received for {Parameters.Timeout}");
LastActionTime = DateTime.UtcNow;
try
{
@@ -680,7 +612,7 @@ namespace CryptoExchange.Net.Sockets
if (DateTime.UtcNow - LastActionTime > Parameters.Timeout)
{
_log.Write(LogLevel.Warning, $"Socket {Id} No data received for {Parameters.Timeout}, reconnecting socket");
_logger.Log(LogLevel.Warning, $"[Sckt {Id}] no data received for {Parameters.Timeout}, reconnecting socket");
_ = ReconnectAsync().ConfigureAwait(false);
return;
}
@@ -700,7 +632,7 @@ namespace CryptoExchange.Net.Sockets
// Because this is running in a separate task and not awaited until the socket gets closed
// any exception here will stop the timeout checking, but do so silently unless the socket get's stopped.
// Make sure we at least let the owner know there was an error
OnError?.Invoke(e);
await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false);
throw;
}
}
@@ -711,20 +643,13 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns>
private static int NextStreamId()
{
lock (streamIdLock)
lock (_streamIdLock)
{
lastStreamId++;
return lastStreamId;
_lastStreamId++;
return _lastStreamId;
}
}
private int MessagesSentLastSecond()
{
var testTime = DateTime.UtcNow;
_outgoingMessages.RemoveAll(r => testTime - r > TimeSpan.FromSeconds(1));
return _outgoingMessages.Count;
}
/// <summary>
/// Update the received messages list, removing messages received longer than 3s ago
/// </summary>
@@ -734,8 +659,10 @@ namespace CryptoExchange.Net.Sockets
if (checkTime - _lastReceivedMessagesUpdate > TimeSpan.FromSeconds(1))
{
foreach (var msg in _receivedMessages.ToList()) // To list here because we're removing from the list
{
if (checkTime - msg.Timestamp > TimeSpan.FromSeconds(3))
_receivedMessages.Remove(msg);
}
_lastReceivedMessagesUpdate = checkTime;
}
@@ -764,6 +691,32 @@ namespace CryptoExchange.Net.Sockets
}
}
/// <summary>
/// Message info
/// </summary>
public struct SendItem
{
/// <summary>
/// The request id
/// </summary>
public int Id { get; set; }
/// <summary>
/// The request id
/// </summary>
public int Weight { get; set; }
/// <summary>
/// Timestamp the request was sent
/// </summary>
public DateTime SendTime { get; set; }
/// <summary>
/// The bytes to send
/// </summary>
public byte[] Bytes { get; set; }
}
/// <summary>
/// Received message info
/// </summary>
@@ -1,43 +0,0 @@
using Newtonsoft.Json.Linq;
using System;
namespace CryptoExchange.Net.Sockets
{
/// <summary>
/// Message received event
/// </summary>
public class MessageEvent
{
/// <summary>
/// The connection the message was received on
/// </summary>
public SocketConnection Connection { get; set; }
/// <summary>
/// The json object of the data
/// </summary>
public JToken JsonData { get; set; }
/// <summary>
/// The originally received string data
/// </summary>
public string? OriginalData { get; set; }
/// <summary>
/// The timestamp of when the data was received
/// </summary>
public DateTime ReceivedTimestamp { get; set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="connection"></param>
/// <param name="jsonData"></param>
/// <param name="originalData"></param>
/// <param name="timestamp"></param>
public MessageEvent(SocketConnection connection, JToken jsonData, string? originalData, DateTime timestamp)
{
Connection = connection;
JsonData = jsonData;
OriginalData = originalData;
ReceivedTimestamp = timestamp;
}
}
}
@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace CryptoExchange.Net.Sockets.MessageParsing.Interfaces
{
/// <summary>
/// Message accessor
/// </summary>
public interface IMessageAccessor
{
/// <summary>
/// Is this a json message
/// </summary>
bool IsJson { get; }
/// <summary>
/// The underlying data object
/// </summary>
object? Underlying { get; }
/// <summary>
/// Load a stream message
/// </summary>
/// <param name="stream"></param>
void Load(Stream stream);
/// <summary>
/// Get the type of node
/// </summary>
/// <returns></returns>
NodeType? GetNodeType();
/// <summary>
/// Get the type of node
/// </summary>
/// <param name="path">Access path</param>
/// <returns></returns>
NodeType? GetNodeType(MessagePath path);
/// <summary>
/// Get the value of a path
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="path"></param>
/// <returns></returns>
T? GetValue<T>(MessagePath path);
/// <summary>
/// Get the values of an array
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="path"></param>
/// <returns></returns>
List<T?>? GetValues<T>(MessagePath path);
/// <summary>
/// Deserialize the message into this type
/// </summary>
/// <param name="type"></param>
/// <param name="path"></param>
/// <returns></returns>
object Deserialize(Type type, MessagePath? path = null);
}
}
@@ -0,0 +1,15 @@
namespace CryptoExchange.Net.Sockets.MessageParsing.Interfaces
{
/// <summary>
/// Serializer interface
/// </summary>
public interface IMessageSerializer
{
/// <summary>
/// Serialize an object to a string
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
string Serialize(object message);
}
}
@@ -0,0 +1,158 @@
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace CryptoExchange.Net.Sockets.MessageParsing
{
/// <summary>
/// Json.Net message accessor
/// </summary>
public class JsonNetMessageAccessor : IMessageAccessor
{
private JToken? _token;
private Stream? _stream;
private static JsonSerializer _serializer = JsonSerializer.Create(SerializerOptions.WithConverters);
/// <inheritdoc />
public bool IsJson { get; private set; }
/// <inheritdoc />
public object? Underlying => _token;
/// <inheritdoc />
public void Load(Stream stream)
{
_stream = stream;
using var reader = new StreamReader(stream, Encoding.UTF8, false, (int)stream.Length, true);
using var jsonTextReader = new JsonTextReader(reader);
try
{
_token = JToken.Load(jsonTextReader);
IsJson = true;
}
catch (Exception)
{
// Not a json message
IsJson = false;
}
}
/// <inheritdoc />
public object Deserialize(Type type, MessagePath? path = null)
{
if (!IsJson)
{
var sr = new StreamReader(_stream);
return sr.ReadToEnd();
}
var source = _token;
if (path != null)
source = GetPathNode(path.Value);
return source!.ToObject(type, _serializer)!;
}
/// <inheritdoc />
public NodeType? GetNodeType()
{
if (_token == null)
return null;
if (_token.Type == JTokenType.Object)
return NodeType.Object;
if (_token.Type == JTokenType.Array)
return NodeType.Array;
return NodeType.Value;
}
/// <inheritdoc />
public NodeType? GetNodeType(MessagePath path)
{
var node = GetPathNode(path);
if (node == null)
return null;
if (node.Type == JTokenType.Object)
return NodeType.Object;
if (node.Type == JTokenType.Array)
return NodeType.Array;
return NodeType.Value;
}
/// <inheritdoc />
public T? GetValue<T>(MessagePath path)
{
var value = GetPathNode(path);
if (value == null)
return default;
if (value.Type == JTokenType.Object || value.Type == JTokenType.Array)
return default;
return value!.Value<T>();
}
/// <inheritdoc />
public List<T?>? GetValues<T>(MessagePath path)
{
var value = GetPathNode(path);
if (value == null)
return default;
if (value.Type == JTokenType.Object)
return default;
return value!.Values<T>().ToList();
}
private JToken? GetPathNode(MessagePath path)
{
var currentToken = _token;
foreach (var node in path)
{
if (node.Type == 0)
{
// Int value
var val = (int)node.Value!;
if (currentToken!.Type != JTokenType.Array || ((JArray)currentToken).Count <= val)
return null;
currentToken = currentToken[val];
}
else if (node.Type == 1)
{
// String value
if (currentToken!.Type != JTokenType.Object)
return null;
currentToken = currentToken[(string)node.Value!];
}
else
{
// Property name
if (currentToken!.Type != JTokenType.Object)
return null;
currentToken = (currentToken.First as JProperty)?.Name;
}
if (currentToken == null)
return null;
}
return currentToken;
}
}
}
@@ -0,0 +1,12 @@
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
using Newtonsoft.Json;
namespace CryptoExchange.Net.Sockets.MessageParsing
{
/// <inheritdoc />
public class JsonNetSerializer : IMessageSerializer
{
/// <inheritdoc />
public string Serialize(object message) => JsonConvert.SerializeObject(message, Formatting.None);
}
}
@@ -0,0 +1,44 @@
namespace CryptoExchange.Net.Sockets.MessageParsing
{
/// <summary>
/// Node accessor
/// </summary>
public struct NodeAccessor
{
/// <summary>
/// Value
/// </summary>
public object? Value { get; }
/// <summary>
/// Type (0 = int, 1 = string, 2 = prop name)
/// </summary>
public int Type { get; }
private NodeAccessor(object? value, int type)
{
Value = value;
Type = type;
}
/// <summary>
/// Create an int node accessor
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static NodeAccessor Int(int value) { return new NodeAccessor(value, 0); }
/// <summary>
/// Create a string node accessor
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static NodeAccessor String(string value) { return new NodeAccessor(value, 1); }
/// <summary>
/// Create a property name node accessor
/// </summary>
/// <returns></returns>
public static NodeAccessor PropertyName() { return new NodeAccessor(null, 2); }
}
}
@@ -0,0 +1,50 @@
using System.Collections;
using System.Collections.Generic;
namespace CryptoExchange.Net.Sockets.MessageParsing
{
/// <summary>
/// Message access definition
/// </summary>
public struct MessagePath : IEnumerable<NodeAccessor>
{
private List<NodeAccessor> _path;
internal void Add(NodeAccessor node)
{
_path.Add(node);
}
/// <summary>
/// ctor
/// </summary>
public MessagePath()
{
_path = new List<NodeAccessor>();
}
/// <summary>
/// Create a new message path
/// </summary>
/// <returns></returns>
public static MessagePath Get()
{
return new MessagePath();
}
/// <summary>
/// IEnumerable implementation
/// </summary>
/// <returns></returns>
public IEnumerator<NodeAccessor> GetEnumerator()
{
for (var i = 0; i < _path.Count; i++)
yield return _path[i];
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}
@@ -0,0 +1,43 @@
namespace CryptoExchange.Net.Sockets.MessageParsing
{
/// <summary>
/// Message path extension methods
/// </summary>
public static class MessagePathExtension
{
/// <summary>
/// Add a string node accessor
/// </summary>
/// <param name="path"></param>
/// <param name="propName"></param>
/// <returns></returns>
public static MessagePath Property(this MessagePath path, string propName)
{
path.Add(NodeAccessor.String(propName));
return path;
}
/// <summary>
/// Add a property name node accessor
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static MessagePath PropertyName(this MessagePath path)
{
path.Add(NodeAccessor.PropertyName());
return path;
}
/// <summary>
/// Add a int node accessor
/// </summary>
/// <param name="path"></param>
/// <param name="index"></param>
/// <returns></returns>
public static MessagePath Index(this MessagePath path, int index)
{
path.Add(NodeAccessor.Int(index));
return path;
}
}
}
@@ -0,0 +1,21 @@
namespace CryptoExchange.Net.Sockets.MessageParsing
{
/// <summary>
/// Message node type
/// </summary>
public enum NodeType
{
/// <summary>
/// Array node
/// </summary>
Array,
/// <summary>
/// Object node
/// </summary>
Object,
/// <summary>
/// Value node
/// </summary>
Value
}
}
@@ -1,51 +0,0 @@
using CryptoExchange.Net.Objects;
using Newtonsoft.Json.Linq;
using System;
using System.Threading;
namespace CryptoExchange.Net.Sockets
{
internal class PendingRequest
{
public Func<JToken, bool> Handler { get; }
public JToken? Result { get; private set; }
public bool Completed { get; private set; }
public AsyncResetEvent Event { get; }
public DateTime RequestTimestamp { get; set; }
public TimeSpan Timeout { get; }
public SocketSubscription? Subscription { get; }
private CancellationTokenSource cts;
public PendingRequest(Func<JToken, bool> handler, TimeSpan timeout, SocketSubscription? subscription)
{
Handler = handler;
Event = new AsyncResetEvent(false, false);
Timeout = timeout;
RequestTimestamp = DateTime.UtcNow;
Subscription = subscription;
cts = new CancellationTokenSource(timeout);
cts.Token.Register(Fail, false);
}
public bool CheckData(JToken data)
{
return Handler(data);
}
public bool Succeed(JToken data)
{
Result = data;
Completed = true;
Event.Set();
return true;
}
public void Fail()
{
Completed = true;
Event.Set();
}
}
}
@@ -0,0 +1,30 @@
using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Sockets
{
/// <summary>
/// Periodic task registration
/// </summary>
public class PeriodicTaskRegistration
{
/// <summary>
/// Identifier
/// </summary>
public string Identifier { get; set; } = string.Empty;
/// <summary>
/// Interval of query
/// </summary>
public TimeSpan Interval { get; set; }
/// <summary>
/// Delegate for getting the query
/// </summary>
public Func<SocketConnection, Query> QueryDelegate { get; set; } = null!;
/// <summary>
/// Callback after query
/// </summary>
public Action<CallResult>? Callback { get; set; }
}
}
+206
View File
@@ -0,0 +1,206 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Sockets
{
/// <summary>
/// Query
/// </summary>
public abstract class Query : IMessageProcessor
{
/// <summary>
/// Unique identifier
/// </summary>
public int Id { get; } = ExchangeHelpers.NextId();
/// <summary>
/// Has this query been completed
/// </summary>
public bool Completed { get; set; }
/// <summary>
/// Timestamp of when the request was send
/// </summary>
public DateTime RequestTimestamp { get; set; }
/// <summary>
/// Result
/// </summary>
public CallResult? Result { get; set; }
/// <summary>
/// Response
/// </summary>
public object? Response { get; set; }
/// <summary>
/// Wait event for the calling message processing thread
/// </summary>
public AsyncResetEvent? ContinueAwaiter { get; set; }
/// <summary>
/// Strings to match this query to a received message
/// </summary>
public abstract HashSet<string> ListenerIdentifiers { get; set; }
/// <summary>
/// The query request object
/// </summary>
public object Request { get; set; }
/// <summary>
/// If this is a private request
/// </summary>
public bool Authenticated { get; }
/// <summary>
/// Weight of the query
/// </summary>
public int Weight { get; }
/// <summary>
/// Get the type the message should be deserialized to
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
public abstract Type? GetMessageType(IMessageAccessor message);
/// <summary>
/// Wait event for response
/// </summary>
protected AsyncResetEvent _event;
/// <summary>
/// Cancellation token
/// </summary>
protected CancellationTokenSource? _cts;
/// <summary>
/// ctor
/// </summary>
/// <param name="request"></param>
/// <param name="authenticated"></param>
/// <param name="weight"></param>
public Query(object request, bool authenticated, int weight = 1)
{
_event = new AsyncResetEvent(false, false);
Authenticated = authenticated;
Request = request;
Weight = weight;
}
/// <summary>
/// Signal that the request has been send and the timeout timer should start
/// </summary>
public void IsSend(TimeSpan timeout)
{
// Start timeout countdown
RequestTimestamp = DateTime.UtcNow;
_cts = new CancellationTokenSource(timeout);
_cts.Token.Register(Timeout, false);
}
/// <summary>
/// Wait untill timeout or the request is competed
/// </summary>
/// <param name="timeout"></param>
/// <returns></returns>
public async Task WaitAsync(TimeSpan timeout) => await _event.WaitAsync(timeout).ConfigureAwait(false);
/// <inheritdoc />
public virtual object Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type);
/// <summary>
/// Mark request as timeout
/// </summary>
public abstract void Timeout();
/// <summary>
/// Mark request as failed
/// </summary>
/// <param name="error"></param>
public abstract void Fail(string error);
/// <summary>
/// Handle a response message
/// </summary>
/// <param name="message"></param>
/// <param name="connection"></param>
/// <returns></returns>
public abstract Task<CallResult> HandleAsync(SocketConnection connection, DataEvent<object> message);
}
/// <summary>
/// Query
/// </summary>
/// <typeparam name="TResponse">Response object type</typeparam>
public abstract class Query<TResponse> : Query
{
/// <inheritdoc />
public override Type? GetMessageType(IMessageAccessor message) => typeof(TResponse);
/// <summary>
/// The typed call result
/// </summary>
public CallResult<TResponse>? TypedResult => (CallResult<TResponse>?)Result;
/// <summary>
/// ctor
/// </summary>
/// <param name="request"></param>
/// <param name="authenticated"></param>
/// <param name="weight"></param>
protected Query(object request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
{
}
/// <inheritdoc />
public override async Task<CallResult> HandleAsync(SocketConnection connection, DataEvent<object> message)
{
Completed = true;
Response = message.Data;
Result = await HandleMessageAsync(connection, message.As((TResponse)message.Data)).ConfigureAwait(false);
_event.Set();
await (ContinueAwaiter?.WaitAsync() ?? Task.CompletedTask).ConfigureAwait(false);
return Result;
}
/// <summary>
/// Handle the query response
/// </summary>
/// <param name="connection"></param>
/// <param name="message"></param>
/// <returns></returns>
public virtual Task<CallResult<TResponse>> HandleMessageAsync(SocketConnection connection, DataEvent<TResponse> message) => Task.FromResult(new CallResult<TResponse>(message.Data, message.OriginalData, null));
/// <inheritdoc />
public override void Timeout()
{
if (Completed)
return;
Completed = true;
Result = new CallResult<TResponse>(new CancellationRequestedError(null, "Query timeout", null));
ContinueAwaiter?.Set();
_event.Set();
}
/// <inheritdoc />
public override void Fail(string error)
{
Result = new CallResult<TResponse>(new ServerError(error));
Completed = true;
ContinueAwaiter?.Set();
_event.Set();
}
}
}

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