diff --git a/CryptoExchange.Net.UnitTests/BaseClientTests.cs b/CryptoExchange.Net.UnitTests/BaseClientTests.cs index bf43577..c109479 100644 --- a/CryptoExchange.Net.UnitTests/BaseClientTests.cs +++ b/CryptoExchange.Net.UnitTests/BaseClientTests.cs @@ -23,7 +23,7 @@ namespace CryptoExchange.Net.UnitTests // arrange // act // assert - Assert.Throws(typeof(ArgumentException), () => new TestBaseClient(new RestClientOptions() { ApiCredentials = new ApiCredentials(key, secret) })); + Assert.Throws(typeof(ArgumentException), () => new TestBaseClient(new RestClientOptions("") { ApiCredentials = new ApiCredentials(key, secret) })); } [TestCase] @@ -31,7 +31,7 @@ namespace CryptoExchange.Net.UnitTests { // arrange var stringBuilder = new StringBuilder(); - var client = new TestBaseClient(new RestClientOptions() + var client = new TestBaseClient(new RestClientOptions("") { LogWriters = new List { new StringWriter(stringBuilder) } }); @@ -67,7 +67,7 @@ namespace CryptoExchange.Net.UnitTests { // arrange var stringBuilder = new StringBuilder(); - var client = new TestBaseClient(new RestClientOptions() + var client = new TestBaseClient(new RestClientOptions("") { LogWriters = new List { new StringWriter(stringBuilder) }, LogVerbosity = verbosity diff --git a/CryptoExchange.Net.UnitTests/RestClientTests.cs b/CryptoExchange.Net.UnitTests/RestClientTests.cs index 78fa402..963e4f4 100644 --- a/CryptoExchange.Net.UnitTests/RestClientTests.cs +++ b/CryptoExchange.Net.UnitTests/RestClientTests.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Net.Http; using System.Text; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.RateLimiter; @@ -105,7 +106,7 @@ namespace CryptoExchange.Net.UnitTests { // arrange // act - var client = new TestRestClient(new RestClientOptions() + var client = new TestRestClient(new RestClientOptions("") { BaseAddress = "http://test.address.com", RateLimiters = new List{new RateLimiterTotal(1, TimeSpan.FromSeconds(1))}, @@ -123,7 +124,7 @@ namespace CryptoExchange.Net.UnitTests public void SettingRateLimitingBehaviourToFail_Should_FailLimitedRequests() { // arrange - var client = new TestRestClient(new RestClientOptions() + var client = new TestRestClient(new RestClientOptions("") { RateLimiters = new List { new RateLimiterTotal(1, TimeSpan.FromSeconds(1)) }, RateLimitingBehaviour = RateLimitingBehaviour.Fail @@ -146,7 +147,7 @@ namespace CryptoExchange.Net.UnitTests public void SettingRateLimitingBehaviourToWait_Should_DelayLimitedRequests() { // arrange - var client = new TestRestClient(new RestClientOptions() + var client = new TestRestClient(new RestClientOptions("") { RateLimiters = new List { new RateLimiterTotal(1, TimeSpan.FromSeconds(1)) }, RateLimitingBehaviour = RateLimitingBehaviour.Wait @@ -171,7 +172,7 @@ namespace CryptoExchange.Net.UnitTests public void SettingApiKeyRateLimiter_Should_DelayRequestsFromSameKey() { // arrange - var client = new TestRestClient(new RestClientOptions() + var client = new TestRestClient(new RestClientOptions("") { RateLimiters = new List { new RateLimiterAPIKey(1, TimeSpan.FromSeconds(1)) }, RateLimitingBehaviour = RateLimitingBehaviour.Wait, diff --git a/CryptoExchange.Net.UnitTests/SocketClientTests.cs b/CryptoExchange.Net.UnitTests/SocketClientTests.cs index 42c7ae1..e7d4049 100644 --- a/CryptoExchange.Net.UnitTests/SocketClientTests.cs +++ b/CryptoExchange.Net.UnitTests/SocketClientTests.cs @@ -19,7 +19,7 @@ namespace CryptoExchange.Net.UnitTests { //arrange //act - var client = new TestSocketClient(new SocketClientOptions() + var client = new TestSocketClient(new SocketClientOptions("") { BaseAddress = "http://test.address.com", ReconnectInterval = TimeSpan.FromSeconds(6) @@ -51,7 +51,7 @@ namespace CryptoExchange.Net.UnitTests public void SocketMessages_Should_BeProcessedInDataHandlers() { // arrange - var client = new TestSocketClient(new SocketClientOptions() { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); + var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); var socket = client.CreateSocket(); socket.ShouldReconnect = true; socket.CanConnect = true; @@ -59,11 +59,11 @@ namespace CryptoExchange.Net.UnitTests var sub = new SocketConnection(client, socket); var rstEvent = new ManualResetEvent(false); JToken result = null; - sub.AddHandler("TestHandler", true, (connection, data) => + sub.AddHandler(SocketSubscription.CreateForIdentifier("TestHandler", true, (connection, data) => { result = data; rstEvent.Set(); - }); + })); client.ConnectSocketSub(sub); // act @@ -79,7 +79,7 @@ namespace CryptoExchange.Net.UnitTests { // arrange bool reconnected = false; - var client = new TestSocketClient(new SocketClientOptions() { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); + var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); var socket = client.CreateSocket(); socket.ShouldReconnect = true; socket.CanConnect = true; @@ -106,12 +106,12 @@ namespace CryptoExchange.Net.UnitTests public void UnsubscribingStream_Should_CloseTheSocket() { // arrange - var client = new TestSocketClient(new SocketClientOptions() { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); + var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); var socket = client.CreateSocket(); socket.CanConnect = true; var sub = new SocketConnection(client, socket); client.ConnectSocketSub(sub); - var ups = new UpdateSubscription(sub, new SocketSubscription("Test", null, true, (d, a) => {})); + var ups = new UpdateSubscription(sub, SocketSubscription.CreateForIdentifier("Test", true, (d, a) => {})); // act client.Unsubscribe(ups).Wait(); @@ -124,7 +124,7 @@ namespace CryptoExchange.Net.UnitTests public void UnsubscribingAll_Should_CloseAllSockets() { // arrange - var client = new TestSocketClient(new SocketClientOptions() { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); + var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); var socket1 = client.CreateSocket(); var socket2 = client.CreateSocket(); socket1.CanConnect = true; @@ -146,7 +146,7 @@ namespace CryptoExchange.Net.UnitTests public void FailingToConnectSocket_Should_ReturnError() { // arrange - var client = new TestSocketClient(new SocketClientOptions() { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); + var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); var socket = client.CreateSocket(); socket.CanConnect = false; var sub = new SocketConnection(client, socket); diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs index dc0cd25..9b36f42 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Net.Http; using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Logging; using CryptoExchange.Net.Objects; @@ -7,7 +8,7 @@ namespace CryptoExchange.Net.UnitTests { public class TestBaseClient: BaseClient { - public TestBaseClient(): base(new RestClientOptions(), null) + public TestBaseClient(): base(new RestClientOptions("http://testurl.url"), null) { } @@ -37,12 +38,12 @@ namespace CryptoExchange.Net.UnitTests { } - public override Dictionary AddAuthenticationToHeaders(string uri, string method, Dictionary parameters, bool signed) + public override Dictionary AddAuthenticationToHeaders(string uri, HttpMethod method, Dictionary parameters, bool signed) { return base.AddAuthenticationToHeaders(uri, method, parameters, signed); } - public override Dictionary AddAuthenticationToParameters(string uri, string method, Dictionary parameters, bool signed) + public override Dictionary AddAuthenticationToParameters(string uri, HttpMethod method, Dictionary parameters, bool signed) { return base.AddAuthenticationToParameters(uri, method, parameters, signed); } diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs index e8bf674..8364895 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs @@ -7,8 +7,10 @@ using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Reflection; using System.Text; +using System.Threading; using System.Threading.Tasks; using CryptoExchange.Net.Authentication; @@ -16,7 +18,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations { public class TestRestClient: RestClient { - public TestRestClient() : base(new RestClientOptions(), null) + public TestRestClient() : base(new RestClientOptions("http://testurl.url"), null) { RequestFactory = new Mock().Object; } @@ -39,36 +41,28 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations responseStream.Seek(0, SeekOrigin.Begin); var response = new Mock(); - response.Setup(c => c.GetResponseStream()).Returns(responseStream); - + response.Setup(c => c.IsSuccessStatusCode).Returns(true); + response.Setup(c => c.GetResponseStream()).Returns(Task.FromResult((Stream)responseStream)); + var request = new Mock(); - request.Setup(c => c.Headers).Returns(new WebHeaderCollection()); request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com")); - request.Setup(c => c.GetRequestStream()).Returns(Task.FromResult(requestStream)); - request.Setup(c => c.GetResponse()).Returns(Task.FromResult(response.Object)); + request.Setup(c => c.GetResponse(It.IsAny())).Returns(Task.FromResult(response.Object)); var factory = Mock.Get(RequestFactory); - factory.Setup(c => c.Create(It.IsAny())) + factory.Setup(c => c.Create(It.IsAny(), It.IsAny())) .Returns(request.Object); } public void SetErrorWithoutResponse(HttpStatusCode code, string message) { - var we = new WebException(); - var r = new HttpWebResponse(); - var re = new HttpResponseMessage(); - - typeof(HttpResponseMessage).GetField("_statusCode", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(re, code); - typeof(HttpWebResponse).GetField("_httpResponseMessage", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(r, re); - typeof(WebException).GetField("_message", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(we, message); - typeof(WebException).GetField("_response", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(we, r); - + var we = new HttpRequestException(); + typeof(HttpRequestException).GetField("_message", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(we, message); + var request = new Mock(); - request.Setup(c => c.Headers).Returns(new WebHeaderCollection()); - request.Setup(c => c.GetResponse()).Throws(we); + request.Setup(c => c.GetResponse(It.IsAny())).Throws(we); var factory = Mock.Get(RequestFactory); - factory.Setup(c => c.Create(It.IsAny())) + factory.Setup(c => c.Create(It.IsAny(), It.IsAny())) .Returns(request.Object); } @@ -79,22 +73,22 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations responseStream.Write(expectedBytes, 0, expectedBytes.Length); responseStream.Seek(0, SeekOrigin.Begin); - var r = new Mock(); - r.Setup(x => x.GetResponseStream()).Returns(responseStream); - var we = new WebException("", null, WebExceptionStatus.Success, r.Object); - + var response = new Mock(); + response.Setup(c => c.IsSuccessStatusCode).Returns(false); + response.Setup(c => c.GetResponseStream()).Returns(Task.FromResult((Stream)responseStream)); + var request = new Mock(); - request.Setup(c => c.Headers).Returns(new WebHeaderCollection()); - request.Setup(c => c.GetResponse()).Throws(we); + request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com")); + request.Setup(c => c.GetResponse(It.IsAny())).Returns(Task.FromResult(response.Object)); var factory = Mock.Get(RequestFactory); - factory.Setup(c => c.Create(It.IsAny())) + factory.Setup(c => c.Create(It.IsAny(), It.IsAny())) .Returns(request.Object); } - public async Task> Request(string method = "GET") where T:class + public async Task> Request(CancellationToken ct = default) where T:class { - return await ExecuteRequest(new Uri("http://www.test.com"), method); + return await SendRequest(new Uri("http://www.test.com"), HttpMethod.Get, ct); } } diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs index d01dc94..43f18b9 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs @@ -11,7 +11,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations { public class TestSocketClient: SocketClient { - public TestSocketClient() : this(new SocketClientOptions()) + public TestSocketClient() : this(new SocketClientOptions("http://testurl.url")) { } @@ -32,32 +32,33 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations return ConnectSocket(sub).Result; } - protected override bool HandleQueryResponse(SocketConnection s, object request, JToken data, out CallResult callResult) + protected internal override bool HandleQueryResponse(SocketConnection s, object request, JToken data, out CallResult callResult) { throw new NotImplementedException(); } - protected override bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message, out CallResult callResult) + protected internal override bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message, + out CallResult callResult) { throw new NotImplementedException(); } - protected override bool MessageMatchesHandler(JToken message, object request) + protected internal override bool MessageMatchesHandler(JToken message, object request) { throw new NotImplementedException(); } - protected override bool MessageMatchesHandler(JToken message, string identifier) + protected internal override bool MessageMatchesHandler(JToken message, string identifier) { return true; } - protected override Task> AuthenticateSocket(SocketConnection s) + protected internal override Task> AuthenticateSocket(SocketConnection s) { throw new NotImplementedException(); } - protected override Task Unsubscribe(SocketConnection connection, SocketSubscription s) + protected internal override Task Unsubscribe(SocketConnection connection, SocketSubscription s) { throw new NotImplementedException(); } diff --git a/CryptoExchange.Net/AssemblyInfo.cs b/CryptoExchange.Net/AssemblyInfo.cs new file mode 100644 index 0000000..987163f --- /dev/null +++ b/CryptoExchange.Net/AssemblyInfo.cs @@ -0,0 +1 @@ +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")] \ No newline at end of file diff --git a/CryptoExchange.Net/Attributes/NullableAttributes.cs b/CryptoExchange.Net/Attributes/NullableAttributes.cs new file mode 100644 index 0000000..cb75272 --- /dev/null +++ b/CryptoExchange.Net/Attributes/NullableAttributes.cs @@ -0,0 +1,210 @@ +#if !NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ + using global::System; + + /// + /// Specifies that is allowed as an input even if the + /// corresponding type disallows it. + /// + [AttributeUsage( + AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, + Inherited = false + )] + [ExcludeFromCodeCoverage] + internal sealed class AllowNullAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + public AllowNullAttribute() { } + } + + /// + /// Specifies that is disallowed as an input even if the + /// corresponding type allows it. + /// + [AttributeUsage( + AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, + Inherited = false + )] + + [ExcludeFromCodeCoverage] + internal sealed class DisallowNullAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + public DisallowNullAttribute() { } + } + + /// + /// Specifies that a method that will never return under any circumstance. + /// + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + [ExcludeFromCodeCoverage] + internal sealed class DoesNotReturnAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + public DoesNotReturnAttribute() { } + } + + /// + /// Specifies that the method will not return if the associated + /// parameter is passed the specified value. + /// + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + [ExcludeFromCodeCoverage] + internal sealed class DoesNotReturnIfAttribute : Attribute + { + /// + /// Gets the condition parameter value. + /// Code after the method is considered unreachable by diagnostics if the argument + /// to the associated parameter matches this value. + /// + public bool ParameterValue { get; } + + /// + /// Initializes a new instance of the + /// class with the specified parameter value. + /// + /// + /// The condition parameter value. + /// Code after the method is considered unreachable by diagnostics if the argument + /// to the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute(bool parameterValue) + { + ParameterValue = parameterValue; + } + } + + /// + /// Specifies that an output may be even if the + /// corresponding type disallows it. + /// + [AttributeUsage( + AttributeTargets.Field | AttributeTargets.Parameter | + AttributeTargets.Property | AttributeTargets.ReturnValue, + Inherited = false + )] + [ExcludeFromCodeCoverage] + internal sealed class MaybeNullAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + public MaybeNullAttribute() { } + } + + /// + /// Specifies that when a method returns , + /// the parameter may be even if the corresponding type disallows it. + /// + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + [ExcludeFromCodeCoverage] + internal sealed class MaybeNullWhenAttribute : Attribute + { + /// + /// Gets the return value condition. + /// If the method returns this value, the associated parameter may be . + /// + public bool ReturnValue { get; } + + /// + /// Initializes the attribute with the specified return value condition. + /// + /// + /// The return value condition. + /// If the method returns this value, the associated parameter may be . + /// + public MaybeNullWhenAttribute(bool returnValue) + { + ReturnValue = returnValue; + } + } + + /// + /// Specifies that an output is not even if the + /// corresponding type allows it. + /// + [AttributeUsage( + AttributeTargets.Field | AttributeTargets.Parameter | + AttributeTargets.Property | AttributeTargets.ReturnValue, + Inherited = false + )] + [ExcludeFromCodeCoverage] + internal sealed class NotNullAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + public NotNullAttribute() { } + } + + /// + /// Specifies that the output will be non- if the + /// named parameter is non-. + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, + AllowMultiple = true, + Inherited = false + )] + [ExcludeFromCodeCoverage] + internal sealed class NotNullIfNotNullAttribute : Attribute + { + /// + /// Gets the associated parameter name. + /// The output will be non- if the argument to the + /// parameter specified is non-. + /// + public string ParameterName { get; } + + /// + /// Initializes the attribute with the associated parameter name. + /// + /// + /// The associated parameter name. + /// The output will be non- if the argument to the + /// parameter specified is non-. + /// + public NotNullIfNotNullAttribute(string parameterName) + { + // .NET Core 3.0 doesn't throw an ArgumentNullException, even though this is + // tagged as non-null. + // Follow this behavior here. + ParameterName = parameterName; + } + } + + /// + /// Specifies that when a method returns , + /// the parameter will not be even if the corresponding type allows it. + /// + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + [ExcludeFromCodeCoverage] + internal sealed class NotNullWhenAttribute : Attribute + { + /// + /// Gets the return value condition. + /// If the method returns this value, the associated parameter will not be . + /// + public bool ReturnValue { get; } + + /// + /// Initializes the attribute with the specified return value condition. + /// + /// + /// The return value condition. + /// If the method returns this value, the associated parameter will not be . + /// + public NotNullWhenAttribute(bool returnValue) + { + ReturnValue = returnValue; + } + } +} +#endif \ No newline at end of file diff --git a/CryptoExchange.Net/BaseClient.cs b/CryptoExchange.Net/BaseClient.cs index 699f349..aa475b6 100644 --- a/CryptoExchange.Net/BaseClient.cs +++ b/CryptoExchange.Net/BaseClient.cs @@ -10,6 +10,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Reflection; +using System.Text; using System.Threading.Tasks; namespace CryptoExchange.Net @@ -22,7 +23,7 @@ namespace CryptoExchange.Net /// /// The address of the client /// - public string BaseAddress { get; private set; } + public string BaseAddress { get; } /// /// The log object /// @@ -207,36 +208,39 @@ namespace CryptoExchange.Net try { - using var reader = new StreamReader(stream); + using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true); using var jsonReader = new JsonTextReader(reader); return new CallResult(serializer.Deserialize(jsonReader), null); } catch (JsonReaderException jre) { + if(stream.CanSeek) + stream.Seek(0, SeekOrigin.Begin); var data = await ReadStream(stream).ConfigureAwait(false); - var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}"; - log.Write(LogVerbosity.Error, info); - return new CallResult(default, new DeserializeError(info)); + log.Write(LogVerbosity.Error, $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}"); + return new CallResult(default, new DeserializeError(data)); } catch (JsonSerializationException jse) { + if (stream.CanSeek) + stream.Seek(0, SeekOrigin.Begin); var data = await ReadStream(stream).ConfigureAwait(false); - var info = $"Deserialize JsonSerializationException: {jse.Message}, data: {data}"; - log.Write(LogVerbosity.Error, info); - return new CallResult(default, new DeserializeError(info)); + log.Write(LogVerbosity.Error, $"Deserialize JsonSerializationException: {jse.Message}, data: {data}"); + return new CallResult(default, new DeserializeError(data)); } catch (Exception ex) { + if (stream.CanSeek) + stream.Seek(0, SeekOrigin.Begin); var data = await ReadStream(stream).ConfigureAwait(false); - var info = $"Deserialize Unknown Exception: {ex.Message}, data: {data}"; - log.Write(LogVerbosity.Error, info); - return new CallResult(default, new DeserializeError(info)); + log.Write(LogVerbosity.Error, $"Deserialize Unknown Exception: {ex.Message}, data: {data}"); + return new CallResult(default, new DeserializeError(data)); } } private async Task ReadStream(Stream stream) - { - using var reader = new StreamReader(stream); + { + using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true); return await reader.ReadToEndAsync().ConfigureAwait(false); } diff --git a/CryptoExchange.Net/Converters/BaseConverter.cs b/CryptoExchange.Net/Converters/BaseConverter.cs index 97f18d0..da8e3d5 100644 --- a/CryptoExchange.Net/Converters/BaseConverter.cs +++ b/CryptoExchange.Net/Converters/BaseConverter.cs @@ -70,7 +70,7 @@ namespace CryptoExchange.Net.Converters return objectType == typeof(T) || Nullable.GetUnderlyingType(objectType) == typeof(T); } - private bool GetValue(string value, [NotNullWhen(false)]out T result) + private bool GetValue(string value, out T result) { var mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase)); if (!mapping.Equals(default(KeyValuePair))) diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj index 783ef3e..99af2ea 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.csproj +++ b/CryptoExchange.Net/CryptoExchange.Net.csproj @@ -1,6 +1,6 @@ - netstandard2.1 + netstandard2.0;netstandard2.1 CryptoExchange.Net @@ -8,12 +8,12 @@ 2.1.8 false https://github.com/JKorf/CryptoExchange.Net - https://github.com/JKorf/CryptoExchange.Net/blob/master/LICENSE en true 2.1.8 - Added array serialization options for implementations enable 8.0 + MIT CryptoExchange.Net.xml diff --git a/CryptoExchange.Net/CryptoExchange.Net.xml b/CryptoExchange.Net/CryptoExchange.Net.xml index 0a8433e..d71e803 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.xml +++ b/CryptoExchange.Net/CryptoExchange.Net.xml @@ -536,11 +536,6 @@ Content - - - Headers - - Method @@ -564,6 +559,13 @@ + + + Add a header to the request + + + + Get the response @@ -663,13 +665,13 @@ Removes all rate limiters from this client - + Ping to see if the server is reachable The roundtrip time of the ping request - + Ping to see if the server is reachable @@ -700,6 +702,20 @@ The base address of the API + + + + + + + + + The max amount of concurrent socket connections + + + + + Unsubscribe from a stream @@ -1135,6 +1151,12 @@ + + + Overwrite bool check so we can use if(callResult) instead of if(callResult.Success) + + + The result of a request @@ -1306,12 +1328,18 @@ The message for the error that occured - + + + Optional data for the error + + + ctor + @@ -1344,51 +1372,53 @@ Error returned by the server - + ctor + - + ctor + Web error returned by the server - + ctor - + Error while deserializing data - + ctor - + Deserializing data Unknown error - + ctor - + Error data @@ -1665,6 +1695,11 @@ The bid list + + + Order book implementation id + + The log @@ -1910,9 +1945,6 @@ - - - @@ -1928,6 +1960,9 @@ + + + @@ -2034,13 +2069,13 @@ Removes all rate limiters from this client - + Ping to see if the server is reachable The roundtrip time of the ping request - + Ping to see if the server is reachable @@ -2380,6 +2415,187 @@ Socket implementation + + + Socket + + + + + Log + + + + + Error handlers + + + + + Open handlers + + + + + Close handlers + + + + + Message handlers + + + + + Id + + + + + If is reconnecting + + + + + Origin + + + + + Url + + + + + Is closed + + + + + Is open + + + + + Protocols + + + + + Interpreter for bytes + + + + + Interpreter for strings + + + + + Last action time + + + + + Timeout + + + + + Socket state + + + + + ctor + + + + + + + ctor + + + + + + + + + On close + + + + + On message + + + + + On error + + + + + On open + + + + + Handle + + + + + + Handle + + + + + + + + Checks if timed out + + + + + + Close socket + + + + + + Reset socket + + + + + Send data + + + + + + Connect socket + + + + + + Set a proxy + + + + + + + Dispose + + Socket connecting @@ -2442,24 +2658,11 @@ The socket client The socket - + - Add a handler + Add handler - The request object - If it is a user subscription or a generic handler - The data handler - - - - - Add a handler - - The identifier of the handler - If it is a user subscription or a generic handler - The data handler - - + @@ -2538,21 +2741,23 @@ If the subscription has been confirmed - + - ctor + Create SocketSubscription for a request + - + - ctor + Create SocketSubscription for an identifier + diff --git a/CryptoExchange.Net/ExtensionMethods.cs b/CryptoExchange.Net/ExtensionMethods.cs index b8133b4..116a904 100644 --- a/CryptoExchange.Net/ExtensionMethods.cs +++ b/CryptoExchange.Net/ExtensionMethods.cs @@ -131,7 +131,7 @@ namespace CryptoExchange.Net /// /// /// - internal static SecureString ToSecureString(this string source) + public static SecureString ToSecureString(this string source) { var secureString = new SecureString(); foreach (var c in source) diff --git a/CryptoExchange.Net/Interfaces/IRequest.cs b/CryptoExchange.Net/Interfaces/IRequest.cs index 656a276..c8b9403 100644 --- a/CryptoExchange.Net/Interfaces/IRequest.cs +++ b/CryptoExchange.Net/Interfaces/IRequest.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Threading; @@ -20,10 +21,6 @@ namespace CryptoExchange.Net.Interfaces /// string? Content { get; } /// - /// Headers - /// - HttpRequestHeaders Headers { get; } - /// /// Method /// HttpMethod Method { get; set; } @@ -42,6 +39,13 @@ namespace CryptoExchange.Net.Interfaces /// /// void SetContent(string data, string contentType); + + /// + /// Add a header to the request + /// + /// + /// + void AddHeader(string key, string value); /// /// Get the response /// diff --git a/CryptoExchange.Net/Interfaces/IRestClient.cs b/CryptoExchange.Net/Interfaces/IRestClient.cs index c2f3b82..60d9e8b 100644 --- a/CryptoExchange.Net/Interfaces/IRestClient.cs +++ b/CryptoExchange.Net/Interfaces/IRestClient.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using CryptoExchange.Net.Objects; using CryptoExchange.Net.RateLimiter; @@ -51,12 +52,12 @@ namespace CryptoExchange.Net.Interfaces /// Ping to see if the server is reachable /// /// The roundtrip time of the ping request - CallResult Ping(); + CallResult Ping(CancellationToken ct = default); /// /// Ping to see if the server is reachable /// /// The roundtrip time of the ping request - Task> PingAsync(); + Task> PingAsync(CancellationToken ct = default); } } \ No newline at end of file diff --git a/CryptoExchange.Net/Interfaces/ISocketClient.cs b/CryptoExchange.Net/Interfaces/ISocketClient.cs index 195b1ce..7cbacc9 100644 --- a/CryptoExchange.Net/Interfaces/ISocketClient.cs +++ b/CryptoExchange.Net/Interfaces/ISocketClient.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using CryptoExchange.Net.Objects; using CryptoExchange.Net.Sockets; namespace CryptoExchange.Net.Interfaces @@ -29,6 +30,20 @@ namespace CryptoExchange.Net.Interfaces /// string BaseAddress { get; } + /// + TimeSpan ResponseTimeout { get; } + + /// + TimeSpan SocketNoDataTimeout { get; } + + /// + /// The max amount of concurrent socket connections + /// + int MaxSocketConnections { get; } + + /// + int SocketCombineTarget { get; } + /// /// Unsubscribe from a stream /// diff --git a/CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs b/CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs index 97c6ce0..a9556dc 100644 --- a/CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs +++ b/CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs @@ -12,7 +12,7 @@ namespace CryptoExchange.Net.Logging private static readonly object openedFilesLock = new object(); private static readonly List openedFiles = new List(); - private StreamWriter logWriter; + private readonly StreamWriter logWriter; private readonly object writeLock; /// diff --git a/CryptoExchange.Net/Objects/CallResult.cs b/CryptoExchange.Net/Objects/CallResult.cs index 78044f4..9253ead 100644 --- a/CryptoExchange.Net/Objects/CallResult.cs +++ b/CryptoExchange.Net/Objects/CallResult.cs @@ -40,7 +40,7 @@ namespace CryptoExchange.Net.Objects /// public static implicit operator bool(CallResult obj) { - return !ReferenceEquals(obj, null) && obj.Success; + return obj?.Success == true; } } @@ -67,7 +67,9 @@ namespace CryptoExchange.Net.Objects /// /// /// - public WebCallResult(HttpStatusCode? code, IEnumerable>>? responseHeaders, [AllowNull] T data, Error? error): base(data, error) + public WebCallResult( + HttpStatusCode? code, + IEnumerable>>? responseHeaders, [AllowNull] T data, Error? error): base(data, error) { ResponseHeaders = responseHeaders; ResponseStatusCode = code; @@ -80,7 +82,7 @@ namespace CryptoExchange.Net.Objects /// public static WebCallResult CreateErrorResult(Error error) { - return new WebCallResult(null, null, default, error); + return new WebCallResult(null, null, default!, error); } /// @@ -90,9 +92,9 @@ namespace CryptoExchange.Net.Objects /// /// /// - public static WebCallResult CreateErrorResult(HttpStatusCode? code, IEnumerable>> responseHeaders, Error error) + public static WebCallResult CreateErrorResult(HttpStatusCode? code, IEnumerable>>? responseHeaders, Error error) { - return new WebCallResult(code, responseHeaders, default, error); + return new WebCallResult(code, responseHeaders, default!, error); } } } diff --git a/CryptoExchange.Net/Objects/Error.cs b/CryptoExchange.Net/Objects/Error.cs index 0836d3e..0a4b838 100644 --- a/CryptoExchange.Net/Objects/Error.cs +++ b/CryptoExchange.Net/Objects/Error.cs @@ -14,15 +14,22 @@ /// public string Message { get; set; } + /// + /// Optional data for the error + /// + public object? Data { get; set; } + /// /// ctor /// /// /// - protected Error(int code, string message) + /// + protected Error(int code, string message, object? data) { Code = code; Message = message; + Data = data; } /// @@ -31,7 +38,7 @@ /// public override string ToString() { - return $"{Code}: {Message}"; + return $"{Code}: {Message} {Data}"; } } @@ -43,7 +50,7 @@ /// /// ctor /// - public CantConnectError() : base(1, "Can't connect to the server") { } + public CantConnectError() : base(1, "Can't connect to the server", null) { } } /// @@ -54,7 +61,7 @@ /// /// ctor /// - public NoApiCredentialsError() : base(2, "No credentials provided for private endpoint") { } + public NoApiCredentialsError() : base(2, "No credentials provided for private endpoint", null) { } } /// @@ -66,14 +73,16 @@ /// ctor /// /// - public ServerError(string message) : base(3, "Server error: " + message) { } + /// + public ServerError(string message, object? data = null) : base(3, "Server error: " + message, data) { } /// /// ctor /// /// /// - public ServerError(int code, string message) : base(code, message) + /// + public ServerError(int code, string message, object? data = null) : base(code, message, data) { } } @@ -86,8 +95,8 @@ /// /// ctor /// - /// - public WebError(string message) : base(4, "Web error: " + message) { } + /// + public WebError(object? data) : base(4, "Web error", data) { } } /// @@ -98,8 +107,8 @@ /// /// ctor /// - /// - public DeserializeError(string message) : base(5, "Error deserializing data: " + message) { } + /// Deserializing data + public DeserializeError(object? data) : base(5, "Error deserializing data", data) { } } /// @@ -110,8 +119,8 @@ /// /// ctor /// - /// - public UnknownError(string message) : base(6, "Unknown error occured " + message) { } + /// Error data + public UnknownError(object? data = null) : base(6, "Unknown error occured", data) { } } /// @@ -123,7 +132,7 @@ /// ctor /// /// - public ArgumentError(string message) : base(7, "Invalid parameter: " + message) { } + public ArgumentError(string message) : base(7, "Invalid parameter: " + message, null) { } } /// @@ -135,7 +144,7 @@ /// ctor /// /// - public RateLimitError(string message) : base(8, "Rate limit exceeded: " + message) { } + public RateLimitError(string message) : base(8, "Rate limit exceeded: " + message, null) { } } /// @@ -146,6 +155,6 @@ /// /// ctor /// - public CancellationRequestedError() : base(9, "Cancellation requested") { } + public CancellationRequestedError() : base(9, "Cancellation requested", null) { } } } diff --git a/CryptoExchange.Net/Objects/Options.cs b/CryptoExchange.Net/Objects/Options.cs index e7e67c7..18744ab 100644 --- a/CryptoExchange.Net/Objects/Options.cs +++ b/CryptoExchange.Net/Objects/Options.cs @@ -153,7 +153,7 @@ namespace CryptoExchange.Net.Objects /// public override string ToString() { - return $"{base.ToString()}, RateLimitters: {RateLimiters.Count}, RateLimitBehaviour: {RateLimitingBehaviour}, RequestTimeout: {RequestTimeout.ToString("c")}"; + return $"{base.ToString()}, RateLimiters: {RateLimiters.Count}, RateLimitBehaviour: {RateLimitingBehaviour}, RequestTimeout: {RequestTimeout:c}"; } } @@ -223,7 +223,7 @@ namespace CryptoExchange.Net.Objects /// public override string ToString() { - return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, SocketResponseTimeout: {SocketResponseTimeout.ToString("c")}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}"; + return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, SocketResponseTimeout: {SocketResponseTimeout:c}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}"; } } } diff --git a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs index 65aab30..675e8e8 100644 --- a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs +++ b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs @@ -33,7 +33,11 @@ namespace CryptoExchange.Net.OrderBook private OrderBookStatus status; private UpdateSubscription? subscription; private readonly bool sequencesAreConsecutive; - private readonly string id; + + /// + /// Order book implementation id + /// + public string Id { get; } /// /// The log /// @@ -54,7 +58,7 @@ namespace CryptoExchange.Net.OrderBook var old = status; status = value; - log.Write(LogVerbosity.Info, $"{id} order book {Symbol} status changed: {old} => {value}"); + log.Write(LogVerbosity.Info, $"{Id} order book {Symbol} status changed: {old} => {value}"); OnStatusChange?.Invoke(old, status); } } @@ -146,12 +150,12 @@ namespace CryptoExchange.Net.OrderBook protected SymbolOrderBook(string symbol, OrderBookOptions options) { if (symbol == null) - throw new ArgumentNullException("symbol"); + throw new ArgumentNullException(nameof(symbol)); if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); - id = options.OrderBookName; + Id = options.OrderBookName; processBuffer = new List(); sequencesAreConsecutive = options.SequenceNumbersAreConsecutive; Symbol = symbol; @@ -191,7 +195,7 @@ namespace CryptoExchange.Net.OrderBook private void Reset() { - log.Write(LogVerbosity.Warning, $"{id} order book {Symbol} connection lost"); + log.Write(LogVerbosity.Warning, $"{Id} order book {Symbol} connection lost"); Status = OrderBookStatus.Connecting; processBuffer.Clear(); bookSet = false; @@ -211,7 +215,7 @@ namespace CryptoExchange.Net.OrderBook success = resyncResult; } - log.Write(LogVerbosity.Info, $"{id} order book {Symbol} successfully resynchronized"); + log.Write(LogVerbosity.Info, $"{Id} order book {Symbol} successfully resynchronized"); Status = OrderBookStatus.Synced; } @@ -278,7 +282,7 @@ namespace CryptoExchange.Net.OrderBook bookSet = true; LastOrderBookUpdate = DateTime.UtcNow; OnOrderBookUpdate?.Invoke(); - log.Write(LogVerbosity.Debug, $"{id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks"); + log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks"); } } @@ -304,12 +308,12 @@ namespace CryptoExchange.Net.OrderBook Entries = entries }; processBuffer.Add(entry); - log.Write(LogVerbosity.Debug, $"{id} order book {Symbol} update before synced; buffering"); + log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update before synced; buffering"); } else if (sequencesAreConsecutive && firstSequenceNumber > LastSequenceNumber + 1) { // Out of sync - log.Write(LogVerbosity.Warning, $"{id} order book {Symbol} out of sync, reconnecting"); + log.Write(LogVerbosity.Warning, $"{Id} order book {Symbol} out of sync, reconnecting"); subscription!.Reconnect().Wait(); } else @@ -320,7 +324,7 @@ namespace CryptoExchange.Net.OrderBook CheckProcessBuffer(); LastOrderBookUpdate = DateTime.UtcNow; OnOrderBookUpdate?.Invoke(); - log.Write(LogVerbosity.Debug, $"{id} order book {Symbol} update: {entries.Count} entries processed"); + log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update: {entries.Count} entries processed"); } } } diff --git a/CryptoExchange.Net/Requests/Request.cs b/CryptoExchange.Net/Requests/Request.cs index 4edbd5f..3387cde 100644 --- a/CryptoExchange.Net/Requests/Request.cs +++ b/CryptoExchange.Net/Requests/Request.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; @@ -11,7 +12,7 @@ namespace CryptoExchange.Net.Requests /// /// Request object /// - internal class Request : IRequest + public class Request : IRequest { private readonly HttpRequestMessage request; private readonly HttpClient httpClient; @@ -26,10 +27,7 @@ namespace CryptoExchange.Net.Requests httpClient = client; this.request = request; } - - /// - public HttpRequestHeaders Headers => request.Headers; - + /// public string? Content { get; private set; } @@ -56,6 +54,12 @@ namespace CryptoExchange.Net.Requests request.Content = new StringContent(data, Encoding.UTF8, contentType); } + /// + public void AddHeader(string key, string value) + { + request.Headers.Add(key, value); + } + /// public void SetContent(byte[] data) { diff --git a/CryptoExchange.Net/Requests/RequestFactory.cs b/CryptoExchange.Net/Requests/RequestFactory.cs index b91e188..2434ca1 100644 --- a/CryptoExchange.Net/Requests/RequestFactory.cs +++ b/CryptoExchange.Net/Requests/RequestFactory.cs @@ -25,8 +25,7 @@ namespace CryptoExchange.Net.Requests } }; - httpClient = new HttpClient(handler); - httpClient.Timeout = requestTimeout; + httpClient = new HttpClient(handler) {Timeout = requestTimeout}; } /// diff --git a/CryptoExchange.Net/RestClient.cs b/CryptoExchange.Net/RestClient.cs index a635a73..11aa854 100644 --- a/CryptoExchange.Net/RestClient.cs +++ b/CryptoExchange.Net/RestClient.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net.Http; @@ -46,11 +47,11 @@ namespace CryptoExchange.Net /// /// Timeout for requests /// - protected TimeSpan RequestTimeout { get; private set; } + protected TimeSpan RequestTimeout { get; } /// /// Rate limiting behaviour /// - public RateLimitingBehaviour RateLimitBehaviour { get; private set; } + public RateLimitingBehaviour RateLimitBehaviour { get; } /// /// List of ratelimitters /// @@ -68,7 +69,7 @@ namespace CryptoExchange.Net protected RestClient(RestClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider): base(exchangeOptions, authenticationProvider) { if (exchangeOptions == null) - throw new ArgumentNullException("Options"); + throw new ArgumentNullException(nameof(exchangeOptions)); RequestTimeout = exchangeOptions.RequestTimeout; RequestFactory.Configure(exchangeOptions.RequestTimeout, exchangeOptions.Proxy); @@ -86,7 +87,7 @@ namespace CryptoExchange.Net public void AddRateLimiter(IRateLimiter limiter) { if (limiter == null) - throw new ArgumentNullException("limiter"); + throw new ArgumentNullException(nameof(limiter)); var rateLimiters = RateLimiters.ToList(); rateLimiters.Add(limiter); @@ -105,17 +106,19 @@ namespace CryptoExchange.Net /// Ping to see if the server is reachable /// /// The roundtrip time of the ping request - public virtual CallResult Ping() => PingAsync().Result; + public virtual CallResult Ping(CancellationToken ct = default) => PingAsync(ct).Result; /// /// Ping to see if the server is reachable /// /// The roundtrip time of the ping request - public virtual async Task> PingAsync() + public virtual async Task> PingAsync(CancellationToken ct = default) { var ping = new Ping(); var uri = new Uri(BaseAddress); PingReply reply; + + var ctRegistration = ct.Register(() => ping.SendAsyncCancel()); try { reply = await ping.SendPingAsync(uri.Host).ConfigureAwait(false); @@ -131,9 +134,13 @@ namespace CryptoExchange.Net } finally { + ctRegistration.Dispose(); ping.Dispose(); } + if(ct.IsCancellationRequested) + return new CallResult(0, new CancellationRequestedError()); + return reply.Status == IPStatus.Success ? new CallResult(reply.RoundtripTime, null) : new CallResult(0, new CantConnectError { Message = "Ping failed: " + reply.Status }); } @@ -148,6 +155,7 @@ namespace CryptoExchange.Net /// Whether or not the request should be authenticated /// Whether or not the resulting object should be checked for missing properties in the mapping (only outputs if log verbosity is Debug) /// + [return: NotNull] protected virtual async Task> SendRequest(Uri uri, HttpMethod method, CancellationToken cancellationToken, Dictionary? parameters = null, bool signed = false, bool checkResult = true) where T : class { @@ -198,13 +206,17 @@ namespace CryptoExchange.Net if (response.IsSuccessStatusCode) { var desResult = await Deserialize(responseStream).ConfigureAwait(false); + responseStream.Close(); response.Close(); + + return new WebCallResult(statusCode, headers, desResult.Data, desResult.Error); } else { using var reader = new StreamReader(responseStream); var data = await reader.ReadToEndAsync().ConfigureAwait(false); + responseStream.Close(); response.Close(); var parseResult = ValidateJson(data); return new WebCallResult(statusCode, headers, default, parseResult.Success ? ParseErrorResponse(parseResult.Data) :new ServerError(data)); @@ -261,7 +273,7 @@ namespace CryptoExchange.Net headers = authProvider.AddAuthenticationToHeaders(uriString, method, parameters!, signed); foreach (var header in headers) - request.Headers.Add(header.Key, header.Value); + request.AddHeader(header.Key, header.Value); if ((method == HttpMethod.Post || method == HttpMethod.Put) && postParametersPosition != PostParameters.InUri) { @@ -286,7 +298,6 @@ namespace CryptoExchange.Net { var stringData = JsonConvert.SerializeObject(parameters.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value)); request.SetContent(stringData, contentType); - } else if(requestBodyFormat == RequestBodyFormat.FormData) { diff --git a/CryptoExchange.Net/SocketClient.cs b/CryptoExchange.Net/SocketClient.cs index ef99cfb..b416553 100644 --- a/CryptoExchange.Net/SocketClient.cs +++ b/CryptoExchange.Net/SocketClient.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -33,13 +34,13 @@ namespace CryptoExchange.Net protected internal readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1); /// - public TimeSpan ReconnectInterval { get; private set; } + public TimeSpan ReconnectInterval { get; } /// - public bool AutoReconnect { get; private set; } + public bool AutoReconnect { get; } /// - public TimeSpan ResponseTimeout { get; private set; } + public TimeSpan ResponseTimeout { get; } /// - public TimeSpan SocketNoDataTimeout { get; private set; } + public TimeSpan SocketNoDataTimeout { get; } /// /// The max amount of concurrent socket connections /// @@ -87,7 +88,7 @@ namespace CryptoExchange.Net protected SocketClient(SocketClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider): base(exchangeOptions, authenticationProvider) { if (exchangeOptions == null) - throw new ArgumentNullException("Options"); + throw new ArgumentNullException(nameof(exchangeOptions)); AutoReconnect = exchangeOptions.AutoReconnect; ReconnectInterval = exchangeOptions.ReconnectInterval; @@ -101,7 +102,7 @@ namespace CryptoExchange.Net /// /// Handler for byte data /// Handler for string data - protected void SetDataInterpreter(Func byteHandler, Func stringHandler) + protected void SetDataInterpreter(Func? byteHandler, Func? stringHandler) { dataInterpreterBytes = byteHandler; dataInterpreterString = stringHandler; @@ -116,7 +117,7 @@ namespace CryptoExchange.Net /// If the subscription should be authenticated /// The handler of update data /// - protected virtual Task> Subscribe(object request, string identifier, bool authenticated, Action dataHandler) + protected virtual Task> Subscribe(object? request, string? identifier, bool authenticated, Action dataHandler) { return Subscribe(BaseAddress, request, identifier, authenticated, dataHandler); } @@ -171,8 +172,10 @@ namespace CryptoExchange.Net } } else + { handler.Confirmed = true; - + } + socket.ShouldReconnect = true; return new CallResult(new UpdateSubscription(socket, handler), null); } @@ -187,7 +190,7 @@ namespace CryptoExchange.Net protected internal virtual async Task> SubscribeAndWait(SocketConnection socket, object request, SocketSubscription subscription) { CallResult? callResult = null; - await socket.SendAndWait(request, ResponseTimeout, data => HandleSubscriptionResponse(socket, subscription, request, data, out var callResult)).ConfigureAwait(false); + await socket.SendAndWait(request, ResponseTimeout, data => HandleSubscriptionResponse(socket, subscription, request, data, out callResult)).ConfigureAwait(false); if (callResult?.Success == true) subscription.Confirmed = true; @@ -312,7 +315,7 @@ namespace CryptoExchange.Net /// The message /// The interpretation (null if message wasn't a response to the request) /// True if the message was a response to the query - protected internal abstract bool HandleQueryResponse(SocketConnection s, object request, JToken data, out CallResult callResult); + protected internal abstract bool HandleQueryResponse(SocketConnection s, object request, JToken data, [NotNullWhen(true)]out CallResult? callResult); /// /// Needs to check if a received message was an answer to a subscription request (preferable by id) and set the callResult out to whatever the response is /// @@ -322,7 +325,7 @@ namespace CryptoExchange.Net /// The message /// The interpretation (null if message wasn't a response to the request) /// True if the message was a response to the subscription request - protected internal abstract bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message, out CallResult callResult); + protected internal abstract bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message, out CallResult? callResult); /// /// Needs to check if a received message matches a handler. Typically if an update message matches the request /// @@ -391,7 +394,11 @@ namespace CryptoExchange.Net dataHandler(desResult.Data); } - return connection.AddHandler(request ?? identifier!, userSubscription, InternalHandler); + var handler = request == null + ? SocketSubscription.CreateForIdentifier(identifier!, userSubscription, InternalHandler) + : SocketSubscription.CreateForRequest(request, userSubscription, InternalHandler); + connection.AddHandler(handler); + return handler; } /// @@ -399,11 +406,12 @@ namespace CryptoExchange.Net /// /// The name of the request handler. Needs to be unique /// The action to execute when receiving a message for this handler (checked by ) - protected virtual void AddGenericHandler(string identifier, Action action) + protected void AddGenericHandler(string identifier, Action action) { genericHandlers.Add(identifier, action); + var handler = SocketSubscription.CreateForIdentifier(identifier, false, action); foreach (var connection in sockets.Values) - connection.AddHandler(identifier, false, action); + connection.AddHandler(handler); } /// @@ -429,7 +437,11 @@ namespace CryptoExchange.Net var socket = CreateSocket(address); var socketWrapper = new SocketConnection(this, socket); foreach (var kvp in genericHandlers) - socketWrapper.AddHandler(kvp.Key, false, kvp.Value); + { + var handler = SocketSubscription.CreateForIdentifier(kvp.Key, false, kvp.Value); + socketWrapper.AddHandler(handler); + } + return socketWrapper; } @@ -468,7 +480,7 @@ namespace CryptoExchange.Net socket.DataInterpreterString = dataInterpreterString; socket.OnError += e => { - log.Write(LogVerbosity.Info, $"Socket {socket.Id} error: " + e.ToString()); + log.Write(LogVerbosity.Info, $"Socket {socket.Id} error: " + e); }; return socket; } @@ -481,7 +493,7 @@ namespace CryptoExchange.Net public virtual void SendPeriodic(TimeSpan interval, Func objGetter) { if (objGetter == null) - throw new ArgumentNullException("objGetter"); + throw new ArgumentNullException(nameof(objGetter)); periodicEvent = new AutoResetEvent(false); periodicTask = Task.Run(async () => @@ -526,7 +538,7 @@ namespace CryptoExchange.Net public virtual async Task Unsubscribe(UpdateSubscription subscription) { if (subscription == null) - throw new ArgumentNullException("subscription"); + throw new ArgumentNullException(nameof(subscription)); log.Write(LogVerbosity.Info, "Closing subscription"); await subscription.Close().ConfigureAwait(false); @@ -538,7 +550,7 @@ namespace CryptoExchange.Net /// public virtual async Task UnsubscribeAll() { - log.Write(LogVerbosity.Debug, $"Closing all {sockets.Sum(s => s.Value.handlers.Count(h => h.UserSubscription))} subscriptions"); + log.Write(LogVerbosity.Debug, $"Closing all {sockets.Sum(s => s.Value.HandlerCount)} subscriptions"); await Task.Run(() => { diff --git a/CryptoExchange.Net/Sockets/BaseSocket.cs b/CryptoExchange.Net/Sockets/BaseSocket.cs index e2a9aa5..a69be1f 100644 --- a/CryptoExchange.Net/Sockets/BaseSocket.cs +++ b/CryptoExchange.Net/Sockets/BaseSocket.cs @@ -16,45 +16,111 @@ namespace CryptoExchange.Net.Sockets /// /// Socket implementation /// - internal class BaseSocket: IWebsocket + public class BaseSocket: IWebsocket { internal static int lastStreamId; private static readonly object streamIdLock = new object(); + /// + /// Socket + /// protected WebSocket? socket; + /// + /// Log + /// protected Log log; - protected object socketLock = new object(); + private readonly object socketLock = new object(); + /// + /// Error handlers + /// protected readonly List> errorHandlers = new List>(); + /// + /// Open handlers + /// protected readonly List openHandlers = new List(); + /// + /// Close handlers + /// protected readonly List closeHandlers = new List(); + /// + /// Message handlers + /// protected readonly List> messageHandlers = new List>(); - protected IDictionary cookies; - protected IDictionary headers; - protected HttpConnectProxy? proxy; + private readonly IDictionary cookies; + private readonly IDictionary headers; + private HttpConnectProxy? proxy; + /// + /// Id + /// public int Id { get; } + /// + /// If is reconnecting + /// public bool Reconnecting { get; set; } + /// + /// Origin + /// public string? Origin { get; set; } + /// + /// Url + /// public string Url { get; } - public bool IsClosed => socket?.State == null ? true: socket.State == WebSocketState.Closed; + /// + /// Is closed + /// + public bool IsClosed => socket?.State == null || socket.State == WebSocketState.Closed; + /// + /// Is open + /// public bool IsOpen => socket?.State == WebSocketState.Open; + /// + /// Protocols + /// public SslProtocols SSLProtocols { get; set; } = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls; + /// + /// Interpreter for bytes + /// public Func? DataInterpreterBytes { get; set; } + /// + /// Interpreter for strings + /// public Func? DataInterpreterString { get; set; } + /// + /// Last action time + /// public DateTime LastActionTime { get; private set; } + /// + /// Timeout + /// public TimeSpan Timeout { get; set; } private Task? timeoutTask; + /// + /// Socket state + /// public WebSocketState SocketState => socket?.State ?? WebSocketState.None; + /// + /// ctor + /// + /// + /// public BaseSocket(Log log, string url):this(log, url, new Dictionary(), new Dictionary()) { } + /// + /// ctor + /// + /// + /// + /// + /// public BaseSocket(Log log, string url, IDictionary cookies, IDictionary headers) { Id = NextStreamId(); @@ -94,27 +160,43 @@ namespace CryptoExchange.Net.Sockets } } + /// + /// On close + /// public event Action OnClose { add => closeHandlers.Add(value); remove => closeHandlers.Remove(value); } + /// + /// On message + /// public event Action OnMessage { add => messageHandlers.Add(value); remove => messageHandlers.Remove(value); } + /// + /// On error + /// public event Action OnError { add => errorHandlers.Add(value); remove => errorHandlers.Remove(value); } + /// + /// On open + /// public event Action OnOpen { add => openHandlers.Add(value); remove => openHandlers.Remove(value); } + /// + /// Handle + /// + /// protected void Handle(List handlers) { LastActionTime = DateTime.UtcNow; @@ -122,6 +204,12 @@ namespace CryptoExchange.Net.Sockets handle?.Invoke(); } + /// + /// Handle + /// + /// + /// + /// protected void Handle(List> handlers, T data) { LastActionTime = DateTime.UtcNow; @@ -129,6 +217,10 @@ namespace CryptoExchange.Net.Sockets handle?.Invoke(data); } + /// + /// Checks if timed out + /// + /// protected async Task CheckTimeout() { while (true) @@ -150,6 +242,10 @@ namespace CryptoExchange.Net.Sockets } } + /// + /// Close socket + /// + /// public virtual async Task Close() { await Task.Run(() => @@ -184,6 +280,9 @@ namespace CryptoExchange.Net.Sockets }).ConfigureAwait(false); } + /// + /// Reset socket + /// public virtual void Reset() { lock (socketLock) @@ -194,11 +293,19 @@ namespace CryptoExchange.Net.Sockets } } + /// + /// Send data + /// + /// public virtual void Send(string data) { socket?.Send(data); } + /// + /// Connect socket + /// + /// public virtual Task Connect() { if (socket == null) @@ -259,7 +366,9 @@ namespace CryptoExchange.Net.Sockets timeoutTask = Task.Run(CheckTimeout); } else + { log?.Write(LogVerbosity.Debug, $"Socket {Id} connection failed, state: " + socket.State); + } } if (socket.State == WebSocketState.Connecting) @@ -269,6 +378,11 @@ namespace CryptoExchange.Net.Sockets }); } + /// + /// Set a proxy + /// + /// + /// public virtual void SetProxy(string host, int port) { proxy = IPAddress.TryParse(host, out var address) @@ -276,6 +390,9 @@ namespace CryptoExchange.Net.Sockets : new HttpConnectProxy(new DnsEndPoint(host, port)); } + /// + /// Dispose + /// public void Dispose() { lock (socketLock) diff --git a/CryptoExchange.Net/Sockets/SocketConnection.cs b/CryptoExchange.Net/Sockets/SocketConnection.cs index 4c8443b..b7e8b9a 100644 --- a/CryptoExchange.Net/Sockets/SocketConnection.cs +++ b/CryptoExchange.Net/Sockets/SocketConnection.cs @@ -65,7 +65,7 @@ namespace CryptoExchange.Net.Sockets /// public bool PausedActivity { get; set; } - internal readonly List handlers; + private readonly List handlers; private readonly object handlersLock = new object(); private bool lostTriggered; @@ -109,38 +109,7 @@ namespace CryptoExchange.Net.Sockets Connected = true; }; } - - /// - /// Add a handler - /// - /// The request object - /// If it is a user subscription or a generic handler - /// The data handler - /// - public SocketSubscription AddHandler(object request, bool userSubscription, Action dataHandler) - { - var handler = new SocketSubscription(request, userSubscription, dataHandler); - lock (handlersLock) - handlers.Add(handler); - return handler; - } - - /// - /// Add a handler - /// - /// The identifier of the handler - /// If it is a user subscription or a generic handler - /// The data handler - /// - /// - public SocketSubscription AddHandler(string identifier, bool userSubscription, Action dataHandler) - { - var handler = new SocketSubscription(identifier, userSubscription, dataHandler); - lock (handlersLock) - handlers.Add(handler); - return handler; - } - + private void ProcessMessage(string data) { log.Write(LogVerbosity.Debug, $"Socket {Socket.Id} received data: " + data); @@ -167,6 +136,16 @@ namespace CryptoExchange.Net.Sockets } } + /// + /// Add handler + /// + /// + public void AddHandler(SocketSubscription handler) + { + lock(handlersLock) + handlers.Add(handler); + } + private bool HandleData(JToken tokenData) { SocketSubscription? currentSubscription = null; diff --git a/CryptoExchange.Net/Sockets/SocketSubscription.cs b/CryptoExchange.Net/Sockets/SocketSubscription.cs index 0f1df22..492ab9d 100644 --- a/CryptoExchange.Net/Sockets/SocketSubscription.cs +++ b/CryptoExchange.Net/Sockets/SocketSubscription.cs @@ -36,30 +36,38 @@ namespace CryptoExchange.Net.Sockets /// public bool Confirmed { get; set; } - /// - /// ctor - /// - /// - /// - /// - public SocketSubscription(object request, bool userSubscription, Action dataHandler) + private SocketSubscription(object? request, string? identifier, bool userSubscription, Action dataHandler) { UserSubscription = userSubscription; MessageHandler = dataHandler; Request = request; + Identifier = identifier; } /// - /// ctor + /// Create SocketSubscription for a request + /// + /// + /// + /// + /// + public static SocketSubscription CreateForRequest(object request, bool userSubscription, + Action dataHandler) + { + return new SocketSubscription(request, null, userSubscription, dataHandler); + } + + /// + /// Create SocketSubscription for an identifier /// /// /// /// - public SocketSubscription(string identifier, bool userSubscription, Action dataHandler) + /// + public static SocketSubscription CreateForIdentifier(string identifier, bool userSubscription, + Action dataHandler) { - UserSubscription = userSubscription; - MessageHandler = dataHandler; - Identifier = identifier; + return new SocketSubscription(null, identifier, userSubscription, dataHandler); } ///