From 0be1bb16e3d48b136016ad66a1ccab6f02a6993f Mon Sep 17 00:00:00 2001 From: Jan Korf Date: Mon, 23 Dec 2024 08:49:58 +0100 Subject: [PATCH] Feature/update settings (#225) Added SetOptions method to update client settings Added SocketConnection parameter to PeriodicQuery callback Added setting of DefaultProxyCredentials on HttpClient instance when client is not provided by DI Added support for overriding request time out per request Changed max wait time for close handshake response from 5 seconds to 1 second Fixed exception in trade tracker when there is no data in the initial snapshot --- CryptoExchange.Net/Clients/BaseApiClient.cs | 11 ++++ CryptoExchange.Net/Clients/RestApiClient.cs | 8 +++ CryptoExchange.Net/Clients/SocketApiClient.cs | 24 +++++++- .../Interfaces/IBaseApiClient.cs | 8 +++ .../Interfaces/IRequestFactory.cs | 9 ++- CryptoExchange.Net/Interfaces/IWebsocket.cs | 5 ++ .../Objects/Options/UpdateOptions.cs | 29 ++++++++++ CryptoExchange.Net/Requests/RequestFactory.cs | 55 +++++++++++-------- .../Sockets/CryptoExchangeWebSocketClient.cs | 10 +++- .../Sockets/PeriodicTaskRegistration.cs | 2 +- CryptoExchange.Net/Sockets/Query.cs | 5 ++ .../Sockets/SocketConnection.cs | 18 +++++- .../Implementations/TestRequestFactory.cs | 2 + .../Testing/Implementations/TestSocket.cs | 2 + .../Trackers/Trades/TradeTracker.cs | 3 +- 15 files changed, 159 insertions(+), 32 deletions(-) create mode 100644 CryptoExchange.Net/Objects/Options/UpdateOptions.cs diff --git a/CryptoExchange.Net/Clients/BaseApiClient.cs b/CryptoExchange.Net/Clients/BaseApiClient.cs index f47c8fdf..8d1889be 100644 --- a/CryptoExchange.Net/Clients/BaseApiClient.cs +++ b/CryptoExchange.Net/Clients/BaseApiClient.cs @@ -93,6 +93,17 @@ public void SetApiCredentials(T credentials) where T : ApiCredentials AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy()); } + /// + public virtual void SetOptions(UpdateOptions options) where T : ApiCredentials + { + ClientOptions.Proxy = options.Proxy; + ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout; + + ApiOptions.ApiCredentials = options.ApiCredentials ?? ClientOptions.ApiCredentials; + if (options.ApiCredentials != null) + AuthenticationProvider = CreateAuthenticationProvider(options.ApiCredentials.Copy()); + } + /// /// Dispose /// diff --git a/CryptoExchange.Net/Clients/RestApiClient.cs b/CryptoExchange.Net/Clients/RestApiClient.cs index 41e22d16..91e999a9 100644 --- a/CryptoExchange.Net/Clients/RestApiClient.cs +++ b/CryptoExchange.Net/Clients/RestApiClient.cs @@ -961,6 +961,14 @@ protected internal IDictionary CreateParameterDictionary(IDictio /// Server time protected virtual Task> GetServerTimestampAsync() => throw new NotImplementedException(); + /// + public override void SetOptions(UpdateOptions options) + { + base.SetOptions(options); + + RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout); + } + internal async Task> SyncTimeAsync() { var timeSyncParams = GetTimeSyncInfo(); diff --git a/CryptoExchange.Net/Clients/SocketApiClient.cs b/CryptoExchange.Net/Clients/SocketApiClient.cs index d27ee3df..1adcba6d 100644 --- a/CryptoExchange.Net/Clients/SocketApiClient.cs +++ b/CryptoExchange.Net/Clients/SocketApiClient.cs @@ -158,7 +158,7 @@ protected virtual void SetDedicatedConnection(string url, bool auth) /// /// /// - protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func queryDelegate, Action? callback) + protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func queryDelegate, Action? callback) { PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration { @@ -422,9 +422,10 @@ public virtual async Task AuthenticateSocketAsync(SocketConnection s result.Error!.Message = "Authentication failed: " + result.Error.Message; return new CallResult(result.Error)!; } + + _logger.Authenticated(socket.SocketId); } - _logger.Authenticated(socket.SocketId); socket.Authenticated = true; return new CallResult(null); } @@ -710,6 +711,25 @@ public virtual async Task PrepareConnectionsAsync() return new CallResult(null); } + /// + public override void SetOptions(UpdateOptions options) + { + var previousProxyIsSet = ClientOptions.Proxy != null; + base.SetOptions(options); + + if ((!previousProxyIsSet && options.Proxy == null) + || !socketConnections.Any()) + { + return; + } + + _logger.LogInformation("Reconnecting websockets to apply proxy"); + + // Update proxy, also triggers reconnect + foreach (var connection in socketConnections) + _ = connection.Value.UpdateProxy(options.Proxy); + } + /// /// Log the current state of connections and subscriptions /// diff --git a/CryptoExchange.Net/Interfaces/IBaseApiClient.cs b/CryptoExchange.Net/Interfaces/IBaseApiClient.cs index 548a196c..9b321b11 100644 --- a/CryptoExchange.Net/Interfaces/IBaseApiClient.cs +++ b/CryptoExchange.Net/Interfaces/IBaseApiClient.cs @@ -1,5 +1,6 @@ using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.SharedApis; using System; @@ -31,5 +32,12 @@ public interface IBaseApiClient /// /// void SetApiCredentials(T credentials) where T : ApiCredentials; + + /// + /// Set new options. Note that when using a proxy this should be provided in the options even when already set before or it will be reset. + /// + /// Api crentials type + /// Options to set + void SetOptions(UpdateOptions options) where T : ApiCredentials; } } \ No newline at end of file diff --git a/CryptoExchange.Net/Interfaces/IRequestFactory.cs b/CryptoExchange.Net/Interfaces/IRequestFactory.cs index 66ff9067..f3cc827c 100644 --- a/CryptoExchange.Net/Interfaces/IRequestFactory.cs +++ b/CryptoExchange.Net/Interfaces/IRequestFactory.cs @@ -24,6 +24,13 @@ public interface IRequestFactory /// Request timeout to use /// Optional shared http client instance /// Optional proxy to use when no http client is provided - void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient=null); + void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null); + + /// + /// Update settings + /// + /// Proxy to use + /// Request timeout to use + void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout); } } diff --git a/CryptoExchange.Net/Interfaces/IWebsocket.cs b/CryptoExchange.Net/Interfaces/IWebsocket.cs index b1e75cb7..721688df 100644 --- a/CryptoExchange.Net/Interfaces/IWebsocket.cs +++ b/CryptoExchange.Net/Interfaces/IWebsocket.cs @@ -93,5 +93,10 @@ public interface IWebsocket: IDisposable /// /// Task CloseAsync(); + + /// + /// Update proxy setting + /// + void UpdateProxy(ApiProxy? proxy); } } diff --git a/CryptoExchange.Net/Objects/Options/UpdateOptions.cs b/CryptoExchange.Net/Objects/Options/UpdateOptions.cs new file mode 100644 index 00000000..9fc9ed04 --- /dev/null +++ b/CryptoExchange.Net/Objects/Options/UpdateOptions.cs @@ -0,0 +1,29 @@ +using CryptoExchange.Net.Authentication; +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.Objects.Options +{ + /// + /// Options to update + /// + public class UpdateOptions where T : ApiCredentials + { + /// + /// Proxy setting. Note that if this is not provided any previously set proxy will be reset + /// + public ApiProxy? Proxy { get; set; } + /// + /// Api credentials + /// + public T? ApiCredentials { get; set; } + /// + /// Request timeout + /// + public TimeSpan? RequestTimeout { get; set; } + } + + /// + public class UpdateOptions : UpdateOptions { } +} diff --git a/CryptoExchange.Net/Requests/RequestFactory.cs b/CryptoExchange.Net/Requests/RequestFactory.cs index 83ea61df..693e91d5 100644 --- a/CryptoExchange.Net/Requests/RequestFactory.cs +++ b/CryptoExchange.Net/Requests/RequestFactory.cs @@ -17,28 +17,7 @@ public class RequestFactory : IRequestFactory public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null) { if (client == null) - { - var handler = new HttpClientHandler(); - try - { - handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; - } - catch (PlatformNotSupportedException) { } - - if (proxy != null) - { - handler.Proxy = new WebProxy - { - Address = new Uri($"{proxy.Host}:{proxy.Port}"), - Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password) - }; - } - - client = new HttpClient(handler) - { - Timeout = requestTimeout - }; - } + client = CreateClient(proxy, requestTimeout); _httpClient = client; } @@ -51,5 +30,37 @@ public IRequest Create(HttpMethod method, Uri uri, int requestId) return new Request(new HttpRequestMessage(method, uri), _httpClient, requestId); } + + /// + public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout) + { + _httpClient = CreateClient(proxy, requestTimeout); + } + + private HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout) + { + var handler = new HttpClientHandler(); + try + { + handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + handler.DefaultProxyCredentials = CredentialCache.DefaultCredentials; + } + catch (PlatformNotSupportedException) { } + + if (proxy != null) + { + handler.Proxy = new WebProxy + { + Address = new Uri($"{proxy.Host}:{proxy.Port}"), + Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password) + }; + } + + var client = new HttpClient(handler) + { + Timeout = requestTimeout + }; + return client; + } } } diff --git a/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs b/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs index fd3fb650..1d364ebd 100644 --- a/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs +++ b/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs @@ -155,6 +155,12 @@ public CryptoExchangeWebSocketClient(ILogger logger, WebSocketParameters websock _baseAddress = $"{Uri.Scheme}://{Uri.Host}"; } + /// + public void UpdateProxy(ApiProxy? proxy) + { + Parameters.Proxy = proxy; + } + /// public virtual async Task ConnectAsync() { @@ -435,8 +441,8 @@ private async Task CloseInternalAsync() { // Wait until we receive close confirmation await Task.Delay(10).ConfigureAwait(false); - if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(5)) - break; // Wait for max 5 seconds, then just abort the connection + if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(1)) + break; // Wait for max 1 second, then just abort the connection } } } diff --git a/CryptoExchange.Net/Sockets/PeriodicTaskRegistration.cs b/CryptoExchange.Net/Sockets/PeriodicTaskRegistration.cs index 8f9ccaf7..9c532bba 100644 --- a/CryptoExchange.Net/Sockets/PeriodicTaskRegistration.cs +++ b/CryptoExchange.Net/Sockets/PeriodicTaskRegistration.cs @@ -23,6 +23,6 @@ public class PeriodicTaskRegistration /// /// Callback after query /// - public Action? Callback { get; set; } + public Action? Callback { get; set; } } } diff --git a/CryptoExchange.Net/Sockets/Query.cs b/CryptoExchange.Net/Sockets/Query.cs index 833abf8b..397ae433 100644 --- a/CryptoExchange.Net/Sockets/Query.cs +++ b/CryptoExchange.Net/Sockets/Query.cs @@ -23,6 +23,11 @@ public abstract class Query : IMessageProcessor /// public bool Completed { get; set; } + /// + /// Timeout for the request + /// + public TimeSpan? RequestTimeout { get; set; } + /// /// The number of required responses. Can be more than 1 when for example subscribing multiple symbols streams in a single request, /// and each symbol receives it's own confirmation response diff --git a/CryptoExchange.Net/Sockets/SocketConnection.cs b/CryptoExchange.Net/Sockets/SocketConnection.cs index 3e4364f7..a1deaaa6 100644 --- a/CryptoExchange.Net/Sockets/SocketConnection.cs +++ b/CryptoExchange.Net/Sockets/SocketConnection.cs @@ -11,6 +11,8 @@ using CryptoExchange.Net.Clients; using CryptoExchange.Net.Logging.Extensions; using System.Threading; +using CryptoExchange.Net.Objects.Options; +using CryptoExchange.Net.Authentication; namespace CryptoExchange.Net.Sockets { @@ -437,7 +439,7 @@ protected virtual Task HandleRequestSentAsync(int requestId) return Task.CompletedTask; } - query.IsSend(ApiClient.ClientOptions.RequestTimeout); + query.IsSend(query.RequestTimeout ?? ApiClient.ClientOptions.RequestTimeout); return Task.CompletedTask; } @@ -583,6 +585,16 @@ protected virtual async Task HandleStreamMessage(WebSocketMessageType type, Read /// public async Task TriggerReconnectAsync() => await _socket.ReconnectAsync().ConfigureAwait(false); + /// + /// Update the proxy setting and reconnect + /// + /// New proxy setting + public async Task UpdateProxy(ApiProxy? proxy) + { + _socket.UpdateProxy(proxy); + await TriggerReconnectAsync().ConfigureAwait(false); + } + /// /// Close the connection /// @@ -988,7 +1000,7 @@ internal async Task ResubscribeAsync(Subscription subscription) /// How often /// Method returning the query to send /// The callback for processing the response - public virtual void QueryPeriodic(string identifier, TimeSpan interval, Func queryDelegate, Action? callback) + public virtual void QueryPeriodic(string identifier, TimeSpan interval, Func queryDelegate, Action? callback) { if (queryDelegate == null) throw new ArgumentNullException(nameof(queryDelegate)); @@ -1020,7 +1032,7 @@ public virtual void QueryPeriodic(string identifier, TimeSpan interval, Func(T data) public Task ReconnectAsync() => throw new NotImplementedException(); public void Dispose() { } + + public void UpdateProxy(ApiProxy? proxy) => throw new NotImplementedException(); } } diff --git a/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs b/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs index 89625668..05bc2090 100644 --- a/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs +++ b/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs @@ -350,7 +350,8 @@ protected void SetInitialData(IEnumerable data) _data.Add(item); } - _firstTimestamp = _data.Min(v => v.Timestamp); + if (_data.Any()) + _firstTimestamp = _data.Min(v => v.Timestamp); ApplyWindow(false); }