From 3c3b5639f59e07fb5c70d120c15a247d32dfab98 Mon Sep 17 00:00:00 2001 From: Jkorf Date: Wed, 1 Dec 2021 13:31:54 +0100 Subject: [PATCH] Refactor clients/options --- .../BaseClientTests.cs | 19 +- CryptoExchange.Net.UnitTests/OptionsTests.cs | 300 ++++++++++++++++++ .../RestClientTests.cs | 14 +- .../SocketClientTests.cs | 2 +- .../TestImplementations/TestBaseClient.cs | 4 +- .../TestImplementations/TestRestClient.cs | 35 +- .../TestImplementations/TestSocketClient.cs | 16 +- CryptoExchange.Net/Clients/BaseApiClient.cs | 57 ++++ .../{ => Clients}/BaseClient.cs | 4 +- .../BaseRestClient.cs} | 26 +- .../BaseSocketClient.cs} | 28 +- .../RestApiClient.cs} | 10 +- .../SocketApiClient.cs} | 8 +- CryptoExchange.Net/Interfaces/IRestClient.cs | 2 +- .../Interfaces/ISocketClient.cs | 2 +- CryptoExchange.Net/Objects/Options.cs | 203 +++++++----- .../Sockets/SocketConnection.cs | 12 +- .../Sockets/UpdateSubscription.cs | 4 +- CryptoExchange.Net/SubClient.cs | 43 --- 19 files changed, 572 insertions(+), 217 deletions(-) create mode 100644 CryptoExchange.Net.UnitTests/OptionsTests.cs create mode 100644 CryptoExchange.Net/Clients/BaseApiClient.cs rename CryptoExchange.Net/{ => Clients}/BaseClient.cs (98%) rename CryptoExchange.Net/{RestClient.cs => Clients/BaseRestClient.cs} (95%) rename CryptoExchange.Net/{SocketClient.cs => Clients/BaseSocketClient.cs} (97%) rename CryptoExchange.Net/{RestSubClient.cs => Clients/RestApiClient.cs} (73%) rename CryptoExchange.Net/{SocketSubClient.cs => Clients/SocketApiClient.cs} (70%) delete mode 100644 CryptoExchange.Net/SubClient.cs diff --git a/CryptoExchange.Net.UnitTests/BaseClientTests.cs b/CryptoExchange.Net.UnitTests/BaseClientTests.cs index c9f536a..7c6dd88 100644 --- a/CryptoExchange.Net.UnitTests/BaseClientTests.cs +++ b/CryptoExchange.Net.UnitTests/BaseClientTests.cs @@ -11,27 +11,12 @@ namespace CryptoExchange.Net.UnitTests [TestFixture()] public class BaseClientTests { - [TestCase(null, null)] - [TestCase("", "")] - [TestCase("test", null)] - [TestCase("test", "")] - [TestCase(null, "test")] - [TestCase("", "test")] - public void SettingEmptyValuesForAPICredentials_Should_ThrowException(string key, string secret) - { - // arrange - // act - // assert - Assert.Throws(typeof(ArgumentException), - () => new RestSubClientOptions() { ApiCredentials = new ApiCredentials(key, secret) }); - } - [TestCase] public void SettingLogOutput_Should_RedirectLogOutput() { // arrange var logger = new TestStringLogger(); - var client = new TestBaseClient(new RestClientOptions() + var client = new TestBaseClient(new BaseRestClientOptions() { LogWriters = new List { logger } }); @@ -71,7 +56,7 @@ namespace CryptoExchange.Net.UnitTests { // arrange var logger = new TestStringLogger(); - var options = new RestClientOptions() + var options = new BaseRestClientOptions() { LogWriters = new List { logger } }; diff --git a/CryptoExchange.Net.UnitTests/OptionsTests.cs b/CryptoExchange.Net.UnitTests/OptionsTests.cs new file mode 100644 index 0000000..a8cfa05 --- /dev/null +++ b/CryptoExchange.Net.UnitTests/OptionsTests.cs @@ -0,0 +1,300 @@ +using CryptoExchange.Net.Authentication; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.UnitTests.TestImplementations; +using Microsoft.Extensions.Logging; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CryptoExchange.Net.UnitTests +{ + [TestFixture()] + public class OptionsTests + { + [TestCase(null, null)] + [TestCase("", "")] + [TestCase("test", null)] + [TestCase("test", "")] + [TestCase(null, "test")] + [TestCase("", "test")] + public void SettingEmptyValuesForAPICredentials_Should_ThrowException(string key, string secret) + { + // arrange + // act + // assert + Assert.Throws(typeof(ArgumentException), + () => new RestApiClientOptions() { ApiCredentials = new ApiCredentials(key, secret) }); + } + + [Test] + public void TestBasicOptionsAreSet() + { + // arrange, act + var options = new TestClientOptions + { + ApiCredentials = new ApiCredentials("123", "456"), + ReceiveWindow = TimeSpan.FromSeconds(10) + }; + + // assert + Assert.AreEqual(options.ReceiveWindow, TimeSpan.FromSeconds(10)); + Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123"); + Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456"); + } + + [Test] + 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" + } + }; + + // 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") + } + }); + + 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"); + } + + [Test] + public void TestClientUsesCorrectOptionsWithDefault() + { + TestClientOptions.Default = new TestClientOptions() + { + ApiCredentials = new ApiCredentials("123", "456"), + Api1Options = new RestApiClientOptions + { + 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"); + } + + [Test] + public void TestClientUsesCorrectOptionsWithOverridingDefault() + { + TestClientOptions.Default = new TestClientOptions() + { + ApiCredentials = new ApiCredentials("123", "456"), + Api1Options = new RestApiClientOptions + { + ApiCredentials = new ApiCredentials("111", "222") + } + }; + + var client = new TestRestClient(new TestClientOptions + { + Api1Options = new RestApiClientOptions + { + ApiCredentials = new ApiCredentials("333", "444") + }, + Api2Options = new RestApiClientOptions() + { + BaseAddress = "http://test.com" + } + }); + + 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"); + } + } + + public class TestClientOptions: BaseRestClientOptions + { + /// + /// Default options for the futures client + /// + public static TestClientOptions Default { get; set; } = new TestClientOptions() + { + Api1Options = new RestApiClientOptions(), + Api2Options = new RestApiClientOptions() + }; + + /// + /// The default receive window for requests + /// + public TimeSpan ReceiveWindow { get; set; } = TimeSpan.FromSeconds(5); + + private RestApiClientOptions _api1Options = new RestApiClientOptions("https://api1.test.com/"); + public RestApiClientOptions Api1Options + { + get => _api1Options; + set => _api1Options.Copy(_api1Options, value); + } + + private RestApiClientOptions _api2Options = new RestApiClientOptions("https://api2.test.com/"); + public RestApiClientOptions Api2Options + { + get => _api2Options; + set => _api2Options.Copy(_api2Options, value); + } + + /// + /// ctor + /// + public TestClientOptions() + { + if (Default == null) + return; + + Copy(this, Default); + } + + /// + /// Copy the values of the def to the input + /// + /// + /// + /// + public new void Copy(T input, T def) where T : TestClientOptions + { + base.Copy(input, def); + + input.ReceiveWindow = def.ReceiveWindow; + + input.Api1Options = new RestApiClientOptions(def.Api1Options); + input.Api2Options = new RestApiClientOptions(def.Api2Options); + } + } +} diff --git a/CryptoExchange.Net.UnitTests/RestClientTests.cs b/CryptoExchange.Net.UnitTests/RestClientTests.cs index 27f071e..9107ed0 100644 --- a/CryptoExchange.Net.UnitTests/RestClientTests.cs +++ b/CryptoExchange.Net.UnitTests/RestClientTests.cs @@ -106,9 +106,9 @@ namespace CryptoExchange.Net.UnitTests { // arrange // act - var client = new TestRestClient(new TestRestClientOptions() + var client = new TestRestClient(new TestClientOptions() { - SubOptions = new RestSubClientOptions + Api1Options = new RestApiClientOptions { BaseAddress = "http://test.address.com", RateLimiters = new List { new RateLimiter() }, @@ -119,9 +119,9 @@ namespace CryptoExchange.Net.UnitTests // assert - Assert.IsTrue(((TestRestClientOptions)client.ClientOptions).SubOptions.BaseAddress == "http://test.address.com"); - Assert.IsTrue(((TestRestClientOptions)client.ClientOptions).SubOptions.RateLimiters.Count == 1); - Assert.IsTrue(((TestRestClientOptions)client.ClientOptions).SubOptions.RateLimitingBehaviour == RateLimitingBehaviour.Fail); + 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(client.ClientOptions.RequestTimeout == TimeSpan.FromMinutes(1)); } @@ -136,9 +136,9 @@ namespace CryptoExchange.Net.UnitTests { // arrange // act - var client = new TestRestClient(new TestRestClientOptions() + var client = new TestRestClient(new TestClientOptions() { - SubOptions = new RestSubClientOptions + Api1Options = new RestApiClientOptions { BaseAddress = "http://test.address.com" } diff --git a/CryptoExchange.Net.UnitTests/SocketClientTests.cs b/CryptoExchange.Net.UnitTests/SocketClientTests.cs index e7d74fb..374a943 100644 --- a/CryptoExchange.Net.UnitTests/SocketClientTests.cs +++ b/CryptoExchange.Net.UnitTests/SocketClientTests.cs @@ -19,7 +19,7 @@ namespace CryptoExchange.Net.UnitTests //act var client = new TestSocketClient(new TestOptions() { - SubOptions = new SubClientOptions + SubOptions = new RestApiClientOptions { BaseAddress = "http://test.address.com" }, diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs index 8414147..adb591e 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs @@ -8,11 +8,11 @@ namespace CryptoExchange.Net.UnitTests { public class TestBaseClient: BaseClient { - public TestBaseClient(): base("Test", new RestClientOptions()) + public TestBaseClient(): base("Test", new BaseClientOptions()) { } - public TestBaseClient(RestClientOptions exchangeOptions) : base("Test", exchangeOptions) + public TestBaseClient(BaseRestClientOptions exchangeOptions) : base("Test", exchangeOptions) { } diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs index 77bc8ad..c9a1263 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs @@ -15,17 +15,19 @@ using System.Collections.Generic; namespace CryptoExchange.Net.UnitTests.TestImplementations { - public class TestRestClient: RestClient + public class TestRestClient: BaseRestClient { - public TestRestSubClient SubClient { get; } + public TestRestApi1Client Api1 { get; } + public TestRestApi2Client Api2 { get; } - public TestRestClient() : this(new TestRestClientOptions()) + public TestRestClient() : this(new TestClientOptions()) { } - public TestRestClient(TestRestClientOptions exchangeOptions) : base("Test", exchangeOptions) + public TestRestClient(TestClientOptions exchangeOptions) : base("Test", exchangeOptions) { - SubClient = new TestRestSubClient(exchangeOptions); + Api1 = new TestRestApi1Client(exchangeOptions); + Api2 = new TestRestApi2Client(exchangeOptions); RequestFactory = new Mock().Object; } @@ -103,26 +105,35 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations public async Task> Request(CancellationToken ct = default) where T:class { - return await SendRequestAsync(SubClient, new Uri("http://www.test.com"), HttpMethod.Get, ct); + return await SendRequestAsync(Api1, new Uri("http://www.test.com"), HttpMethod.Get, ct); } public async Task> RequestWithParams(HttpMethod method, Dictionary parameters, Dictionary headers) where T : class { - return await SendRequestAsync(SubClient, new Uri("http://www.test.com"), method, default, parameters, additionalHeaders: headers); + return await SendRequestAsync(Api1, new Uri("http://www.test.com"), method, default, parameters, additionalHeaders: headers); } } - public class TestRestSubClient: RestSubClient + public class TestRestApi1Client : RestApiClient { - public TestRestSubClient(TestRestClientOptions options): base(options.SubOptions, null) + public TestRestApi1Client(TestClientOptions options): base(options, options.Api1Options) { } + + public override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) + => new TestAuthProvider(credentials); } - public class TestRestClientOptions: RestClientOptions + public class TestRestApi2Client : RestApiClient { - public RestSubClientOptions SubOptions { get; set; } = new RestSubClientOptions(); + public TestRestApi2Client(TestClientOptions options) : base(options, options.Api2Options) + { + + } + + public override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) + => new TestAuthProvider(credentials); } public class TestAuthProvider : AuthenticationProvider @@ -135,7 +146,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations public class ParseErrorTestRestClient: TestRestClient { public ParseErrorTestRestClient() { } - public ParseErrorTestRestClient(TestRestClientOptions exchangeOptions) : base(exchangeOptions) { } + public ParseErrorTestRestClient(TestClientOptions exchangeOptions) : base(exchangeOptions) { } protected override Error ParseErrorResponse(JToken error) { diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs index 644ad1a..0da2f13 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Logging; using CryptoExchange.Net.Objects; @@ -9,7 +10,7 @@ using Newtonsoft.Json.Linq; namespace CryptoExchange.Net.UnitTests.TestImplementations { - public class TestSocketClient: SocketClient + public class TestSocketClient: BaseSocketClient { public TestSubSocketClient SubClient { get; } @@ -19,7 +20,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations public TestSocketClient(TestOptions exchangeOptions) : base("test", exchangeOptions) { - SubClient = new TestSubSocketClient(exchangeOptions.SubOptions); + SubClient = new TestSubSocketClient(exchangeOptions, exchangeOptions.SubOptions); SocketFactory = new Mock().Object; Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny(), It.IsAny())).Returns(new TestSocket()); } @@ -67,17 +68,20 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations } } - public class TestOptions: SocketClientOptions + public class TestOptions: BaseSocketClientOptions { - public SubClientOptions SubOptions { get; set; } = new SubClientOptions(); + public ApiClientOptions SubOptions { get; set; } = new ApiClientOptions(); } - public class TestSubSocketClient : SocketSubClient + public class TestSubSocketClient : SocketApiClient { - public TestSubSocketClient(SubClientOptions options): base(options, options.ApiCredentials == null ? null: new TestAuthProvider(options.ApiCredentials)) + public TestSubSocketClient(BaseClientOptions options, ApiClientOptions apiOptions): base(options, apiOptions) { } + + public override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) + => new TestAuthProvider(credentials); } } diff --git a/CryptoExchange.Net/Clients/BaseApiClient.cs b/CryptoExchange.Net/Clients/BaseApiClient.cs new file mode 100644 index 0000000..58b8f78 --- /dev/null +++ b/CryptoExchange.Net/Clients/BaseApiClient.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using CryptoExchange.Net.Authentication; +using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Requests; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace CryptoExchange.Net +{ + /// + /// Base rest client + /// + public abstract class BaseApiClient: IDisposable + { + private ApiCredentials? _apiCredentials; + private AuthenticationProvider _authenticationProvider; + public AuthenticationProvider? AuthenticationProvider + { + get + { + if (_authenticationProvider == null && _apiCredentials != null) + _authenticationProvider = CreateAuthenticationProvider(_apiCredentials); + + return _authenticationProvider; + } + } + + internal protected string BaseAddress { get; } + + public BaseApiClient(BaseClientOptions options, ApiClientOptions apiOptions) + { + _apiCredentials = apiOptions.ApiCredentials ?? options.ApiCredentials; + BaseAddress = apiOptions.BaseAddress; + } + + public abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials); + + /// + /// Dispose + /// + public void Dispose() + { + AuthenticationProvider?.Credentials?.Dispose(); + } + } +} diff --git a/CryptoExchange.Net/BaseClient.cs b/CryptoExchange.Net/Clients/BaseClient.cs similarity index 98% rename from CryptoExchange.Net/BaseClient.cs rename to CryptoExchange.Net/Clients/BaseClient.cs index 830cc90..bfb4b0e 100644 --- a/CryptoExchange.Net/BaseClient.cs +++ b/CryptoExchange.Net/Clients/BaseClient.cs @@ -46,14 +46,14 @@ namespace CryptoExchange.Net /// /// Provided client options /// - public ClientOptions ClientOptions { get; } + public BaseClientOptions ClientOptions { get; } /// /// ctor /// /// The name of the exchange this client is for /// The options for this client - protected BaseClient(string exchangeName, ClientOptions options) + protected BaseClient(string exchangeName, BaseClientOptions options) { log = new Log(exchangeName); log.UpdateWriters(options.LogWriters); diff --git a/CryptoExchange.Net/RestClient.cs b/CryptoExchange.Net/Clients/BaseRestClient.cs similarity index 95% rename from CryptoExchange.Net/RestClient.cs rename to CryptoExchange.Net/Clients/BaseRestClient.cs index 34bb5aa..2b995f3 100644 --- a/CryptoExchange.Net/RestClient.cs +++ b/CryptoExchange.Net/Clients/BaseRestClient.cs @@ -21,7 +21,7 @@ namespace CryptoExchange.Net /// /// Base rest client /// - public abstract class RestClient : BaseClient, IRestClient + public abstract class BaseRestClient : BaseClient, IRestClient { /// /// The factory for creating requests. Used for unit testing @@ -70,14 +70,14 @@ namespace CryptoExchange.Net /// /// Client options /// - public new RestClientOptions ClientOptions { get; } + public new BaseRestClientOptions ClientOptions { get; } /// /// ctor /// /// The name of the exchange this client is for /// The options for this client - protected RestClient(string exchangeName, RestClientOptions exchangeOptions) : base(exchangeName, exchangeOptions) + protected BaseRestClient(string exchangeName, BaseRestClientOptions exchangeOptions) : base(exchangeName, exchangeOptions) { if (exchangeOptions == null) throw new ArgumentNullException(nameof(exchangeOptions)); @@ -104,7 +104,7 @@ namespace CryptoExchange.Net /// [return: NotNull] protected virtual async Task> SendRequestAsync( - RestSubClient subClient, + RestApiClient apiClient, Uri uri, HttpMethod method, CancellationToken cancellationToken, @@ -119,17 +119,17 @@ namespace CryptoExchange.Net { var requestId = NextId(); log.Write(LogLevel.Debug, $"[{requestId}] Creating request for " + uri); - if (signed && subClient.AuthenticationProvider == null) + if (signed && apiClient.AuthenticationProvider == null) { log.Write(LogLevel.Warning, $"[{requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided"); return new WebCallResult(null, null, null, new NoApiCredentialsError()); } var paramsPosition = parameterPosition ?? ParameterPositions[method]; - var request = ConstructRequest(subClient, uri, method, parameters, signed, paramsPosition, arraySerialization ?? this.arraySerialization, requestId, additionalHeaders); - foreach (var limiter in subClient.RateLimiters) + var request = ConstructRequest(apiClient, uri, method, parameters, signed, paramsPosition, arraySerialization ?? this.arraySerialization, requestId, additionalHeaders); + foreach (var limiter in apiClient.RateLimiters) { - var limitResult = await limiter.LimitRequestAsync(log, uri.AbsolutePath, method, signed, subClient.Options.ApiCredentials?.Key, subClient.Options.RateLimitingBehaviour, requestWeight, cancellationToken).ConfigureAwait(false); + var limitResult = await limiter.LimitRequestAsync(log, uri.AbsolutePath, method, signed, apiClient.Options.ApiCredentials?.Key, apiClient.Options.RateLimitingBehaviour, requestWeight, cancellationToken).ConfigureAwait(false); if (!limitResult.Success) return new WebCallResult(null, null, null, limitResult.Error); } @@ -267,7 +267,7 @@ namespace CryptoExchange.Net /// Additional headers to send with the request /// protected virtual IRequest ConstructRequest( - SubClient subClient, + RestApiClient apiClient, Uri uri, HttpMethod method, Dictionary? parameters, @@ -280,8 +280,8 @@ namespace CryptoExchange.Net parameters ??= new Dictionary(); var uriString = uri.ToString(); - if (subClient.AuthenticationProvider != null) - parameters = subClient.AuthenticationProvider.AddAuthenticationToParameters(uriString, method, parameters, signed, parameterPosition, arraySerialization); + if (apiClient.AuthenticationProvider != null) + parameters = apiClient.AuthenticationProvider.AddAuthenticationToParameters(uriString, method, parameters, signed, parameterPosition, arraySerialization); if (parameterPosition == HttpMethodParameterPosition.InUri && parameters?.Any() == true) uriString += "?" + parameters.CreateParamString(true, arraySerialization); @@ -291,8 +291,8 @@ namespace CryptoExchange.Net request.Accept = Constants.JsonContentHeader; var headers = new Dictionary(); - if (subClient.AuthenticationProvider != null) - headers = subClient.AuthenticationProvider.AddAuthenticationToHeaders(uriString, method, parameters!, signed, parameterPosition, arraySerialization); + if (apiClient.AuthenticationProvider != null) + headers = apiClient.AuthenticationProvider.AddAuthenticationToHeaders(uriString, method, parameters!, signed, parameterPosition, arraySerialization); foreach (var header in headers) request.AddHeader(header.Key, header.Value); diff --git a/CryptoExchange.Net/SocketClient.cs b/CryptoExchange.Net/Clients/BaseSocketClient.cs similarity index 97% rename from CryptoExchange.Net/SocketClient.cs rename to CryptoExchange.Net/Clients/BaseSocketClient.cs index 3073a6f..3028a0a 100644 --- a/CryptoExchange.Net/SocketClient.cs +++ b/CryptoExchange.Net/Clients/BaseSocketClient.cs @@ -19,7 +19,7 @@ namespace CryptoExchange.Net /// /// Base for socket client implementations /// - public abstract class SocketClient: BaseClient, ISocketClient + public abstract class BaseSocketClient: BaseClient, ISocketClient { #region fields /// @@ -95,7 +95,7 @@ namespace CryptoExchange.Net /// /// Client options /// - public new SocketClientOptions ClientOptions { get; } + public new BaseSocketClientOptions ClientOptions { get; } #endregion @@ -104,7 +104,7 @@ namespace CryptoExchange.Net /// /// The name of the exchange this client is for /// The options for this client - protected SocketClient(string exchangeName, SocketClientOptions exchangeOptions): base(exchangeName, exchangeOptions) + protected BaseSocketClient(string exchangeName, BaseSocketClientOptions exchangeOptions): base(exchangeName, exchangeOptions) { if (exchangeOptions == null) throw new ArgumentNullException(nameof(exchangeOptions)); @@ -133,9 +133,9 @@ namespace CryptoExchange.Net /// The handler of update data /// Cancellation token for closing this subscription /// - protected virtual Task> SubscribeAsync(SocketSubClient subClient, object? request, string? identifier, bool authenticated, Action> dataHandler, CancellationToken ct) + protected virtual Task> SubscribeAsync(SocketApiClient apiClient, object? request, string? identifier, bool authenticated, Action> dataHandler, CancellationToken ct) { - return SubscribeAsync(subClient, subClient.Options.BaseAddress, request, identifier, authenticated, dataHandler, ct); + return SubscribeAsync(apiClient, apiClient.Options.BaseAddress, request, identifier, authenticated, dataHandler, ct); } /// @@ -149,7 +149,7 @@ namespace CryptoExchange.Net /// The handler of update data /// Cancellation token for closing this subscription /// - protected virtual async Task> SubscribeAsync(SocketSubClient subClient, string url, object? request, string? identifier, bool authenticated, Action> dataHandler, CancellationToken ct) + protected virtual async Task> SubscribeAsync(SocketApiClient apiClient, string url, object? request, string? identifier, bool authenticated, Action> dataHandler, CancellationToken ct) { SocketConnection socketConnection; SocketSubscription subscription; @@ -168,7 +168,7 @@ namespace CryptoExchange.Net try { // Get a new or existing socket connection - socketConnection = GetSocketConnection(subClient, url, authenticated); + socketConnection = GetSocketConnection(apiClient, url, authenticated); // Add a subscription on the socket connection subscription = AddSubscription(request, identifier, true, socketConnection, dataHandler); @@ -253,9 +253,9 @@ namespace CryptoExchange.Net /// The request to send, will be serialized to json /// If the query is to an authenticated endpoint /// - protected virtual Task> QueryAsync(SocketSubClient subClient, object request, bool authenticated) + protected virtual Task> QueryAsync(SocketApiClient apiClient, object request, bool authenticated) { - return QueryAsync(subClient, subClient.Options.BaseAddress, request, authenticated); + return QueryAsync(apiClient, apiClient.Options.BaseAddress, request, authenticated); } /// @@ -266,14 +266,14 @@ namespace CryptoExchange.Net /// The request to send /// Whether the socket should be authenticated /// - protected virtual async Task> QueryAsync(SocketSubClient subClient, string url, object request, bool authenticated) + protected virtual async Task> QueryAsync(SocketApiClient apiClient, string url, object request, bool authenticated) { SocketConnection socketConnection; var released = false; await semaphoreSlim.WaitAsync().ConfigureAwait(false); try { - socketConnection = GetSocketConnection(subClient, url, authenticated); + socketConnection = GetSocketConnection(apiClient, url, authenticated); if (ClientOptions.SocketSubscriptionsCombineTarget == 1) { // Can release early when only a single sub per connection @@ -480,10 +480,10 @@ namespace CryptoExchange.Net /// The address the socket is for /// Whether the socket should be authenticated /// - protected virtual SocketConnection GetSocketConnection(SocketSubClient subClient, string address, bool authenticated) + protected virtual SocketConnection GetSocketConnection(SocketApiClient apiClient, string address, bool authenticated) { var socketResult = sockets.Where(s => s.Value.Socket.Url.TrimEnd('/') == address.TrimEnd('/') - && (s.Value.SubClient.GetType() == subClient.GetType()) + && (s.Value.ApiClient.GetType() == apiClient.GetType()) && (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.SubscriptionCount).FirstOrDefault(); var result = socketResult.Equals(default(KeyValuePair)) ? null : socketResult.Value; if (result != null) @@ -497,7 +497,7 @@ namespace CryptoExchange.Net // Create new socket var socket = CreateSocket(address); - var socketConnection = new SocketConnection(this, subClient, socket); + var socketConnection = new SocketConnection(this, apiClient, socket); socketConnection.UnhandledMessage += HandleUnhandledMessage; foreach (var kvp in genericHandlers) { diff --git a/CryptoExchange.Net/RestSubClient.cs b/CryptoExchange.Net/Clients/RestApiClient.cs similarity index 73% rename from CryptoExchange.Net/RestSubClient.cs rename to CryptoExchange.Net/Clients/RestApiClient.cs index f77a7d5..29e892f 100644 --- a/CryptoExchange.Net/RestSubClient.cs +++ b/CryptoExchange.Net/Clients/RestApiClient.cs @@ -21,21 +21,21 @@ namespace CryptoExchange.Net /// /// Base rest client /// - public abstract class RestSubClient: SubClient + public abstract class RestApiClient: BaseApiClient { - internal RestSubClientOptions Options { get; } + internal RestApiClientOptions Options { get; } /// /// List of rate limiters /// internal IEnumerable RateLimiters { get; } - public RestSubClient(RestSubClientOptions options, AuthenticationProvider? authProvider): base(options,authProvider) + public RestApiClient(BaseRestClientOptions options, RestApiClientOptions apiOptions): base(options, apiOptions) { - Options = options; + Options = apiOptions; var rateLimiters = new List(); - foreach (var rateLimiter in options.RateLimiters) + foreach (var rateLimiter in apiOptions.RateLimiters) rateLimiters.Add(rateLimiter); RateLimiters = rateLimiters; } diff --git a/CryptoExchange.Net/SocketSubClient.cs b/CryptoExchange.Net/Clients/SocketApiClient.cs similarity index 70% rename from CryptoExchange.Net/SocketSubClient.cs rename to CryptoExchange.Net/Clients/SocketApiClient.cs index 5f2fb0f..5c85547 100644 --- a/CryptoExchange.Net/SocketSubClient.cs +++ b/CryptoExchange.Net/Clients/SocketApiClient.cs @@ -21,13 +21,13 @@ namespace CryptoExchange.Net /// /// Base rest client /// - public abstract class SocketSubClient : SubClient + public abstract class SocketApiClient : BaseApiClient { - internal SubClientOptions Options { get; } + internal ApiClientOptions Options { get; } - public SocketSubClient(SubClientOptions options, AuthenticationProvider? authProvider): base(options,authProvider) + public SocketApiClient(BaseClientOptions options, ApiClientOptions apiOptions): base(options, apiOptions) { - Options = options; + Options = apiOptions; } } diff --git a/CryptoExchange.Net/Interfaces/IRestClient.cs b/CryptoExchange.Net/Interfaces/IRestClient.cs index e235f62..1eabc49 100644 --- a/CryptoExchange.Net/Interfaces/IRestClient.cs +++ b/CryptoExchange.Net/Interfaces/IRestClient.cs @@ -21,6 +21,6 @@ namespace CryptoExchange.Net.Interfaces /// /// The options provided for this client /// - RestClientOptions ClientOptions { get; } + BaseRestClientOptions ClientOptions { get; } } } \ No newline at end of file diff --git a/CryptoExchange.Net/Interfaces/ISocketClient.cs b/CryptoExchange.Net/Interfaces/ISocketClient.cs index b18c835..7502334 100644 --- a/CryptoExchange.Net/Interfaces/ISocketClient.cs +++ b/CryptoExchange.Net/Interfaces/ISocketClient.cs @@ -13,7 +13,7 @@ namespace CryptoExchange.Net.Interfaces /// /// The options provided for this client /// - SocketClientOptions ClientOptions { get; } + BaseSocketClientOptions ClientOptions { get; } /// /// Incoming kilobytes per second of data diff --git a/CryptoExchange.Net/Objects/Options.cs b/CryptoExchange.Net/Objects/Options.cs index 23fd689..d423a60 100644 --- a/CryptoExchange.Net/Objects/Options.cs +++ b/CryptoExchange.Net/Objects/Options.cs @@ -50,26 +50,18 @@ namespace CryptoExchange.Net.Objects } /// - /// Base for order book options + /// Base client options /// - public class OrderBookOptions : BaseOptions + public class BaseClientOptions : BaseOptions { /// - /// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages. + /// Proxy to use when connecting /// - public bool ChecksumValidationEnabled { get; set; } = true; - } - - public class SubClientOptions - { - /// - /// The base address of the sub client - /// - public string BaseAddress { get; set; } + public ApiProxy? Proxy { get; set; } /// - /// The api credentials used for signing requests - /// + /// Api credentials to be used for all api clients unless overriden + /// public ApiCredentials? ApiCredentials { get; set; } /// @@ -78,40 +70,12 @@ namespace CryptoExchange.Net.Objects /// /// /// - public new void Copy(T input, T def) where T : SubClientOptions - { - input.BaseAddress = def.BaseAddress; - input.ApiCredentials = def.ApiCredentials?.Copy(); - } - - /// - public override string ToString() - { - return $"{base.ToString()}, Credentials: {(ApiCredentials == null ? "-" : "Set")}, BaseAddress: {BaseAddress}"; - } - } - - /// - /// Base client options - /// - public class ClientOptions : BaseOptions - { - /// - /// Proxy to use when connecting - /// - public ApiProxy? Proxy { get; set; } - - /// - /// Copy the values of the def to the input - /// - /// - /// - /// - public new void Copy(T input, T def) where T : ClientOptions + public new void Copy(T input, T def) where T : BaseClientOptions { base.Copy(input, def); input.Proxy = def.Proxy; + input.ApiCredentials = def.ApiCredentials?.Copy(); } /// @@ -121,43 +85,10 @@ namespace CryptoExchange.Net.Objects } } - public class RestSubClientOptions: SubClientOptions - { - /// - /// List of rate limiters to use - /// - public List RateLimiters { get; set; } = new List(); - - /// - /// What to do when a call would exceed the rate limit - /// - public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait; - - /// - /// Copy the values of the def to the input - /// - /// - /// - /// - public new void Copy(T input, T def) where T : RestSubClientOptions - { - base.Copy(input, def); - - input.RateLimiters = def.RateLimiters.ToList(); - input.RateLimitingBehaviour = def.RateLimitingBehaviour; - } - - /// - public override string ToString() - { - return $"{base.ToString()}, RateLimiters: {RateLimiters.Count}, RateLimitBehaviour: {RateLimitingBehaviour}"; - } - } - /// /// Base for rest client options /// - public class RestClientOptions : ClientOptions + public class BaseRestClientOptions : BaseClientOptions { /// /// The time the server has to respond to a request before timing out @@ -175,10 +106,10 @@ namespace CryptoExchange.Net.Objects /// /// /// - public new void Copy(T input, T def) where T : RestClientOptions + public new void Copy(T input, T def) where T : BaseRestClientOptions { base.Copy(input, def); - + input.HttpClient = def.HttpClient; input.RequestTimeout = def.RequestTimeout; } @@ -186,14 +117,14 @@ namespace CryptoExchange.Net.Objects /// public override string ToString() { - return $"{base.ToString()}, RequestTimeout: {RequestTimeout:c}, HttpClient: {(HttpClient == null ? "-": "set")}"; + return $"{base.ToString()}, RequestTimeout: {RequestTimeout:c}, HttpClient: {(HttpClient == null ? "-" : "set")}"; } } /// /// Base for socket client options /// - public class SocketClientOptions : ClientOptions + public class BaseSocketClientOptions : BaseClientOptions { /// /// Whether or not the socket should automatically reconnect when losing connection @@ -244,7 +175,7 @@ namespace CryptoExchange.Net.Objects /// /// /// - public new void Copy(T input, T def) where T : SocketClientOptions + public new void Copy(T input, T def) where T : BaseSocketClientOptions { base.Copy(input, def); @@ -264,4 +195,110 @@ namespace CryptoExchange.Net.Objects return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, MaxReconnectTries: {MaxReconnectTries}, MaxResubscribeTries: {MaxResubscribeTries}, MaxConcurrentResubscriptionsPerSocket: {MaxConcurrentResubscriptionsPerSocket}, SocketResponseTimeout: {SocketResponseTimeout:c}, SocketNoDataTimeout: {SocketNoDataTimeout}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}"; } } + + public class ApiClientOptions + { + /// + /// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property + /// + public string BaseAddress { get; set; } + + /// + /// The api credentials used for signing requests + /// + public ApiCredentials? ApiCredentials { get; set; } + + + public ApiClientOptions() + { + } + + public ApiClientOptions(string baseAddres) + { + BaseAddress = baseAddres; + } + + public ApiClientOptions(ApiClientOptions baseOn) + { + Copy(this, baseOn); + } + + /// + /// Copy the values of the def to the input + /// + /// + /// + /// + public void Copy(T input, T def) where T : ApiClientOptions + { + if (def.BaseAddress != null) + input.BaseAddress = def.BaseAddress; + input.ApiCredentials = def.ApiCredentials?.Copy(); + } + + /// + public override string ToString() + { + return $"{base.ToString()}, Credentials: {(ApiCredentials == null ? "-" : "Set")}, BaseAddress: {BaseAddress}"; + } + } + + public class RestApiClientOptions: ApiClientOptions + { + /// + /// List of rate limiters to use + /// + public List RateLimiters { get; set; } = new List(); + + /// + /// What to do when a call would exceed the rate limit + /// + public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait; + + public RestApiClientOptions() + { + } + + public RestApiClientOptions(string baseAddress): base(baseAddress) + { + } + + public RestApiClientOptions(RestApiClientOptions baseOn) + { + Copy(this, baseOn); + } + + /// + /// Copy the values of the def to the input + /// + /// + /// + /// + public new void Copy(T input, T def) where T : RestApiClientOptions + { + base.Copy(input, def); + + if(def.RateLimiters != null) + input.RateLimiters = def.RateLimiters.ToList(); + input.RateLimitingBehaviour = def.RateLimitingBehaviour; + } + + /// + public override string ToString() + { + return $"{base.ToString()}, RateLimiters: {RateLimiters?.Count}, RateLimitBehaviour: {RateLimitingBehaviour}"; + } + } + + /// + /// Base for order book options + /// + public class OrderBookOptions : BaseOptions + { + /// + /// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages. + /// + public bool ChecksumValidationEnabled { get; set; } = true; + } + } diff --git a/CryptoExchange.Net/Sockets/SocketConnection.cs b/CryptoExchange.Net/Sockets/SocketConnection.cs index 530f7e1..e817f20 100644 --- a/CryptoExchange.Net/Sockets/SocketConnection.cs +++ b/CryptoExchange.Net/Sockets/SocketConnection.cs @@ -77,7 +77,10 @@ namespace CryptoExchange.Net.Sockets /// public IWebsocket Socket { get; set; } - public SocketSubClient SubClient { get; set; } + /// + /// The API client the connection is for + /// + public SocketApiClient ApiClient { get; set; } /// /// If the socket should be reconnected upon closing @@ -128,7 +131,7 @@ namespace CryptoExchange.Net.Sockets private bool lostTriggered; private readonly Log log; - private readonly SocketClient socketClient; + private readonly BaseSocketClient socketClient; private readonly List pendingRequests; @@ -136,12 +139,13 @@ namespace CryptoExchange.Net.Sockets /// New socket connection /// /// The socket client + /// The api client /// The socket - public SocketConnection(SocketClient client, SocketSubClient subClient, IWebsocket socket) + public SocketConnection(BaseSocketClient client, SocketApiClient apiClient, IWebsocket socket) { log = client.log; socketClient = client; - SubClient = subClient; + ApiClient = apiClient; pendingRequests = new List(); diff --git a/CryptoExchange.Net/Sockets/UpdateSubscription.cs b/CryptoExchange.Net/Sockets/UpdateSubscription.cs index 39fa5d0..50ef025 100644 --- a/CryptoExchange.Net/Sockets/UpdateSubscription.cs +++ b/CryptoExchange.Net/Sockets/UpdateSubscription.cs @@ -22,8 +22,8 @@ namespace CryptoExchange.Net.Sockets } /// - /// Event when the connection is closed. This event happens when reconnecting/resubscribing has failed too often based on the and options, - /// or is false. The socket will not be reconnected + /// Event when the connection is closed. This event happens when reconnecting/resubscribing has failed too often based on the and options, + /// or is false. The socket will not be reconnected /// public event Action ConnectionClosed { diff --git a/CryptoExchange.Net/SubClient.cs b/CryptoExchange.Net/SubClient.cs deleted file mode 100644 index 72a7e86..0000000 --- a/CryptoExchange.Net/SubClient.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using System.Web; -using CryptoExchange.Net.Authentication; -using CryptoExchange.Net.Interfaces; -using CryptoExchange.Net.Objects; -using CryptoExchange.Net.Requests; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace CryptoExchange.Net -{ - /// - /// Base rest client - /// - public abstract class SubClient: IDisposable - { - public AuthenticationProvider? AuthenticationProvider { get; } - protected string BaseAddress { get; } - - public SubClient(SubClientOptions options, AuthenticationProvider? authProvider) - { - AuthenticationProvider = authProvider; - BaseAddress = options.BaseAddress; - } - - /// - /// Dispose - /// - public void Dispose() - { - AuthenticationProvider?.Credentials?.Dispose(); - } - } -}