From 73764970b0a292436498f383fa9e6e1c5d0d5d37 Mon Sep 17 00:00:00 2001 From: JKorf Date: Mon, 25 Aug 2025 19:11:50 +0200 Subject: [PATCH] . --- .../TestImplementations/TestSocket.cs | 132 -- CryptoExchange.Net/.editorconfig | 1 + .../Authentication/AuthenticationProvider.cs | 4 + CryptoExchange.Net/Clients/BaseApiClient.cs | 11 +- CryptoExchange.Net/Clients/BaseClient.cs | 22 +- .../Clients/CryptoBaseClient.cs | 14 +- CryptoExchange.Net/Clients/RestApiClient.cs | 1283 ++++++++--------- CryptoExchange.Net/Clients/SocketApiClient.cs | 7 +- .../Objects/AsyncAutoResetEvent.cs | 14 +- .../Objects/RequestDefinition.cs | 169 ++- .../Objects/RequestDefinitionCache.cs | 205 ++- CryptoExchange.Net/Objects/TraceLogger.cs | 18 +- .../OrderBook/SymbolOrderBook.cs | 25 +- .../Sockets/CryptoExchangeWebSocketClient.cs | 11 +- CryptoExchange.Net/Sockets/Query.cs | 1 - .../Sockets/SocketConnection.cs | 25 +- .../Testing/RestRequestValidator.cs | 10 +- 17 files changed, 957 insertions(+), 995 deletions(-) delete mode 100644 CryptoExchange.Net.UnitTests/TestImplementations/TestSocket.cs diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocket.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocket.cs deleted file mode 100644 index c3408de..0000000 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocket.cs +++ /dev/null @@ -1,132 +0,0 @@ -//using System; -//using System.IO; -//using System.Net.WebSockets; -//using System.Security.Authentication; -//using System.Text; -//using System.Threading.Tasks; -//using CryptoExchange.Net.Interfaces; -//using CryptoExchange.Net.Objects; - -//namespace CryptoExchange.Net.UnitTests.TestImplementations -//{ -// public class TestSocket: IWebsocket -// { -// public bool CanConnect { get; set; } -// public bool Connected { get; set; } - -// public event Func OnClose; -//#pragma warning disable 0067 -// public event Func OnReconnected; -// public event Func OnReconnecting; -// public event Func OnRequestRateLimited; -//#pragma warning restore 0067 -// public event Func OnRequestSent; -// public event Func, Task> OnStreamMessage; -// public event Func OnError; -// public event Func OnOpen; -// public Func> GetReconnectionUrl { get; set; } - -// public int Id { get; } -// public bool ShouldReconnect { get; set; } -// public TimeSpan Timeout { get; set; } -// public Func DataInterpreterString { get; set; } -// public Func DataInterpreterBytes { get; set; } -// public DateTime? DisconnectTime { get; set; } -// public string Url { get; } -// public bool IsClosed => !Connected; -// public bool IsOpen => Connected; -// public bool PingConnection { get; set; } -// public TimeSpan PingInterval { get; set; } -// public SslProtocols SSLProtocols { get; set; } -// public Encoding Encoding { get; set; } - -// public int ConnectCalls { get; private set; } -// public bool Reconnecting { get; set; } -// public string Origin { get; set; } -// public int? RatelimitPerSecond { get; set; } - -// public double IncomingKbps => throw new NotImplementedException(); - -// public Uri Uri => new Uri(""); - -// public TimeSpan KeepAliveInterval { get; set; } - -// public static int lastId = 0; -// public static object lastIdLock = new object(); - -// public TestSocket() -// { -// lock (lastIdLock) -// { -// Id = lastId + 1; -// lastId++; -// } -// } - -// public Task ConnectAsync() -// { -// Connected = CanConnect; -// ConnectCalls++; -// if (CanConnect) -// InvokeOpen(); -// return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError())); -// } - -// public bool Send(int requestId, string data, int weight) -// { -// if(!Connected) -// throw new Exception("Socket not connected"); -// OnRequestSent?.Invoke(requestId); -// return true; -// } - -// public void Reset() -// { -// } - -// public Task CloseAsync() -// { -// Connected = false; -// DisconnectTime = DateTime.UtcNow; -// OnClose?.Invoke(); -// return Task.FromResult(0); -// } - -// public void SetProxy(string host, int port) -// { -// throw new NotImplementedException(); -// } -// public void Dispose() -// { -// } - -// public void InvokeClose() -// { -// Connected = false; -// DisconnectTime = DateTime.UtcNow; -// Reconnecting = true; -// OnClose?.Invoke(); -// } - -// public void InvokeOpen() -// { -// OnOpen?.Invoke(); -// } - -// public void InvokeMessage(string data) -// { -// OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory(Encoding.UTF8.GetBytes(data))).Wait(); -// } - -// public void SetProxy(ApiProxy proxy) -// { -// throw new NotImplementedException(); -// } - -// public void InvokeError(Exception error) -// { -// OnError?.Invoke(error); -// } -// public Task ReconnectAsync() => Task.CompletedTask; -// } -//} diff --git a/CryptoExchange.Net/.editorconfig b/CryptoExchange.Net/.editorconfig index 3f12941..bd79d97 100644 --- a/CryptoExchange.Net/.editorconfig +++ b/CryptoExchange.Net/.editorconfig @@ -7,6 +7,7 @@ indent_style = space indent_size = 4 trim_trailing_whitespace = true charset = utf-8 +max_line_length = 140 insert_final_newline = true # ReSharper code style properties diff --git a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs index 56c8ec0..8632710 100644 --- a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs +++ b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs @@ -465,7 +465,11 @@ public abstract class AuthenticationProvider public abstract class AuthenticationProvider : AuthenticationProvider where TApiCredentials : ApiCredentials { /// +#pragma warning disable IDE1006 // Naming Styles +#pragma warning disable CA1707 // Naming Styles protected new TApiCredentials _credentials => (TApiCredentials)base._credentials; +#pragma warning restore IDE1006 // Naming Styles +#pragma warning restore CA1707 // Naming Styles /// /// ctor diff --git a/CryptoExchange.Net/Clients/BaseApiClient.cs b/CryptoExchange.Net/Clients/BaseApiClient.cs index 4c5739a..1900a91 100644 --- a/CryptoExchange.Net/Clients/BaseApiClient.cs +++ b/CryptoExchange.Net/Clients/BaseApiClient.cs @@ -124,7 +124,16 @@ public abstract class BaseApiClient : IDisposable, IBaseApiClient /// /// Dispose /// - public virtual void Dispose() + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Dispose + /// + public virtual void Dispose(bool disposing) { _disposing = true; } diff --git a/CryptoExchange.Net/Clients/BaseClient.cs b/CryptoExchange.Net/Clients/BaseClient.cs index 6a148c9..f5a78df 100644 --- a/CryptoExchange.Net/Clients/BaseClient.cs +++ b/CryptoExchange.Net/Clients/BaseClient.cs @@ -119,10 +119,24 @@ public abstract class BaseClient : IDisposable /// /// Dispose /// - public virtual void Dispose() + public void Dispose() { - _logger.Log(LogLevel.Debug, "Disposing client"); - foreach (var client in ApiClients) - client.Dispose(); + Dispose(true); + GC.SuppressFinalize(this); } + + /// + /// Dispose + /// + public virtual void Dispose(bool disposing) + { + if (disposing) + { + _logger.Log(LogLevel.Debug, "Disposing client"); + foreach (var client in ApiClients) + client.Dispose(); + } + } + + } diff --git a/CryptoExchange.Net/Clients/CryptoBaseClient.cs b/CryptoExchange.Net/Clients/CryptoBaseClient.cs index 4252dcc..417fb14 100644 --- a/CryptoExchange.Net/Clients/CryptoBaseClient.cs +++ b/CryptoExchange.Net/Clients/CryptoBaseClient.cs @@ -56,11 +56,23 @@ public class CryptoBaseClient : IDisposable return result; } + /// + /// Dispose + /// + public void Dispose(bool disposing) + { + if (disposing) + { + _serviceCache.Clear(); + } + } + /// /// Dispose /// public void Dispose() { - _serviceCache.Clear(); + Dispose(true); + GC.SuppressFinalize(this); } } diff --git a/CryptoExchange.Net/Clients/RestApiClient.cs b/CryptoExchange.Net/Clients/RestApiClient.cs index aa98487..d754ee8 100644 --- a/CryptoExchange.Net/Clients/RestApiClient.cs +++ b/CryptoExchange.Net/Clients/RestApiClient.cs @@ -18,711 +18,710 @@ using CryptoExchange.Net.RateLimiting.Interfaces; using CryptoExchange.Net.Requests; using Microsoft.Extensions.Logging; -namespace CryptoExchange.Net.Clients +namespace CryptoExchange.Net.Clients; + +/// +/// Base rest API client for interacting with a REST API +/// +public abstract class RestApiClient : BaseApiClient, IRestApiClient { + /// + public IRequestFactory RequestFactory { get; set; } = new RequestFactory(); + + /// + public abstract TimeSyncInfo? GetTimeSyncInfo(); + + /// + public abstract TimeSpan? GetTimeOffset(); + + /// + public int TotalRequestsMade { get; set; } + /// - /// Base rest API client for interacting with a REST API + /// Request body content type /// - public abstract class RestApiClient : BaseApiClient, IRestApiClient + protected internal RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json; + + /// + /// How to serialize array parameters when making requests + /// + protected internal ArrayParametersSerialization ArraySerialization = ArrayParametersSerialization.Array; + + /// + /// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody) + /// + protected internal string RequestBodyEmptyContent = "{}"; + + /// + /// Request headers to be sent with each request + /// + protected Dictionary StandardRequestHeaders { get; set; } = []; + + /// + /// Whether parameters need to be ordered + /// + protected internal bool OrderParameters { get; set; } = true; + + /// + /// Parameter order comparer + /// + protected IComparer ParameterOrderComparer { get; } = new OrderedStringComparer(); + + /// + /// Where to put the parameters for requests with different Http methods + /// + public Dictionary ParameterPositions { get; set; } = new Dictionary { - /// - public IRequestFactory RequestFactory { get; set; } = new RequestFactory(); + { HttpMethod.Get, HttpMethodParameterPosition.InUri }, + { HttpMethod.Post, HttpMethodParameterPosition.InBody }, + { HttpMethod.Delete, HttpMethodParameterPosition.InBody }, + { HttpMethod.Put, HttpMethodParameterPosition.InBody }, + { new HttpMethod("Patch"), HttpMethodParameterPosition.InBody }, + }; - /// - public abstract TimeSyncInfo? GetTimeSyncInfo(); + /// + public new RestExchangeOptions ClientOptions => (RestExchangeOptions)base.ClientOptions; - /// - public abstract TimeSpan? GetTimeOffset(); + /// + public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions; - /// - public int TotalRequestsMade { get; set; } + /// + /// Memory cache + /// + private readonly static MemoryCache _cache = new MemoryCache(); - /// - /// Request body content type - /// - protected internal RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json; + /// + /// ctor + /// + /// Logger + /// HttpClient to use + /// Base address for this API client + /// The base client options + /// The Api client options + public RestApiClient(ILogger logger, HttpClient? httpClient, string baseAddress, RestExchangeOptions options, RestApiOptions apiOptions) + : base(logger, + apiOptions.OutputOriginalData ?? options.OutputOriginalData, + apiOptions.ApiCredentials ?? options.ApiCredentials, + baseAddress, + options, + apiOptions) + { + RequestFactory.Configure(options.Proxy, options.RequestTimeout, httpClient); + } - /// - /// How to serialize array parameters when making requests - /// - protected internal ArrayParametersSerialization ArraySerialization = ArrayParametersSerialization.Array; + /// + /// Create a message accessor instance + /// + /// + protected abstract IStreamMessageAccessor CreateAccessor(); - /// - /// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody) - /// - protected internal string RequestBodyEmptyContent = "{}"; + /// + /// Create a serializer instance + /// + /// + protected abstract IMessageSerializer CreateSerializer(); - /// - /// Request headers to be sent with each request - /// - protected Dictionary StandardRequestHeaders { get; set; } = []; + /// + /// Send a request to the base address based on the request definition + /// + /// Host and schema + /// Request definition + /// Request parameters + /// Cancellation token + /// Additional headers for this request + /// Override the request weight for this request definition, for example when the weight depends on the parameters + /// + protected virtual async Task SendAsync( + string baseAddress, + RequestDefinition definition, + ParameterCollection? parameters, + CancellationToken cancellationToken, + Dictionary? additionalHeaders = null, + int? weight = null) + { + var result = await SendAsync(baseAddress, definition, parameters, cancellationToken, additionalHeaders, weight).ConfigureAwait(false); + return result.AsDataless(); + } - /// - /// Whether parameters need to be ordered - /// - protected internal bool OrderParameters { get; set; } = true; + /// + /// Send a request to the base address based on the request definition + /// + /// Response type + /// Host and schema + /// Request definition + /// Request parameters + /// Cancellation token + /// Additional headers for this request + /// Override the request weight for this request definition, for example when the weight depends on the parameters + /// Specify the weight to apply to the individual rate limit guard for this request + /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. + /// + protected virtual Task> SendAsync( + string baseAddress, + RequestDefinition definition, + ParameterCollection? parameters, + CancellationToken cancellationToken, + Dictionary? additionalHeaders = null, + int? weight = null, + int? weightSingleLimiter = null, + string? rateLimitKeySuffix = null) + { + var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method]; + return SendAsync( + baseAddress, + definition, + parameterPosition == HttpMethodParameterPosition.InUri ? parameters : null, + parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null, + cancellationToken, + additionalHeaders, + weight, + weightSingleLimiter, + rateLimitKeySuffix); + } - /// - /// Parameter order comparer - /// - protected IComparer ParameterOrderComparer { get; } = new OrderedStringComparer(); - - /// - /// Where to put the parameters for requests with different Http methods - /// - public Dictionary ParameterPositions { get; set; } = new Dictionary + /// + /// Send a request to the base address based on the request definition + /// + /// Response type + /// Host and schema + /// Request definition + /// Request query parameters + /// Request body parameters + /// Cancellation token + /// Additional headers for this request + /// Override the request weight for this request definition, for example when the weight depends on the parameters + /// Specify the weight to apply to the individual rate limit guard for this request + /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. + /// + protected virtual async Task> SendAsync( + string baseAddress, + RequestDefinition definition, + ParameterCollection? uriParameters, + ParameterCollection? bodyParameters, + CancellationToken cancellationToken, + Dictionary? additionalHeaders = null, + int? weight = null, + int? weightSingleLimiter = null, + string? rateLimitKeySuffix = null) + { + string? cacheKey = null; + if (ShouldCache(definition)) { - { HttpMethod.Get, HttpMethodParameterPosition.InUri }, - { HttpMethod.Post, HttpMethodParameterPosition.InBody }, - { HttpMethod.Delete, HttpMethodParameterPosition.InBody }, - { HttpMethod.Put, HttpMethodParameterPosition.InBody }, - { new HttpMethod("Patch"), HttpMethodParameterPosition.InBody }, - }; - - /// - public new RestExchangeOptions ClientOptions => (RestExchangeOptions)base.ClientOptions; - - /// - public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions; - - /// - /// Memory cache - /// - private readonly static MemoryCache _cache = new MemoryCache(); - - /// - /// ctor - /// - /// Logger - /// HttpClient to use - /// Base address for this API client - /// The base client options - /// The Api client options - public RestApiClient(ILogger logger, HttpClient? httpClient, string baseAddress, RestExchangeOptions options, RestApiOptions apiOptions) - : base(logger, - apiOptions.OutputOriginalData ?? options.OutputOriginalData, - apiOptions.ApiCredentials ?? options.ApiCredentials, - baseAddress, - options, - apiOptions) - { - RequestFactory.Configure(options.Proxy, options.RequestTimeout, httpClient); - } - - /// - /// Create a message accessor instance - /// - /// - protected abstract IStreamMessageAccessor CreateAccessor(); - - /// - /// Create a serializer instance - /// - /// - protected abstract IMessageSerializer CreateSerializer(); - - /// - /// Send a request to the base address based on the request definition - /// - /// Host and schema - /// Request definition - /// Request parameters - /// Cancellation token - /// Additional headers for this request - /// Override the request weight for this request definition, for example when the weight depends on the parameters - /// - protected virtual async Task SendAsync( - string baseAddress, - RequestDefinition definition, - ParameterCollection? parameters, - CancellationToken cancellationToken, - Dictionary? additionalHeaders = null, - int? weight = null) - { - var result = await SendAsync(baseAddress, definition, parameters, cancellationToken, additionalHeaders, weight).ConfigureAwait(false); - return result.AsDataless(); - } - - /// - /// Send a request to the base address based on the request definition - /// - /// Response type - /// Host and schema - /// Request definition - /// Request parameters - /// Cancellation token - /// Additional headers for this request - /// Override the request weight for this request definition, for example when the weight depends on the parameters - /// Specify the weight to apply to the individual rate limit guard for this request - /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. - /// - protected virtual Task> SendAsync( - string baseAddress, - RequestDefinition definition, - ParameterCollection? parameters, - CancellationToken cancellationToken, - Dictionary? additionalHeaders = null, - int? weight = null, - int? weightSingleLimiter = null, - string? rateLimitKeySuffix = null) - { - var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method]; - return SendAsync( - baseAddress, - definition, - parameterPosition == HttpMethodParameterPosition.InUri ? parameters : null, - parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null, - cancellationToken, - additionalHeaders, - weight, - weightSingleLimiter, - rateLimitKeySuffix); - } - - /// - /// Send a request to the base address based on the request definition - /// - /// Response type - /// Host and schema - /// Request definition - /// Request query parameters - /// Request body parameters - /// Cancellation token - /// Additional headers for this request - /// Override the request weight for this request definition, for example when the weight depends on the parameters - /// Specify the weight to apply to the individual rate limit guard for this request - /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. - /// - protected virtual async Task> SendAsync( - string baseAddress, - RequestDefinition definition, - ParameterCollection? uriParameters, - ParameterCollection? bodyParameters, - CancellationToken cancellationToken, - Dictionary? additionalHeaders = null, - int? weight = null, - int? weightSingleLimiter = null, - string? rateLimitKeySuffix = null) - { - string? cacheKey = null; - if (ShouldCache(definition)) + cacheKey = baseAddress + definition + uriParameters?.ToFormData(); + _logger.CheckingCache(cacheKey); + var cachedValue = _cache.Get(cacheKey, ClientOptions.CachingMaxAge); + if (cachedValue != null) { - cacheKey = baseAddress + definition + uriParameters?.ToFormData(); - _logger.CheckingCache(cacheKey); - var cachedValue = _cache.Get(cacheKey, ClientOptions.CachingMaxAge); - if (cachedValue != null) - { - _logger.CacheHit(cacheKey); - var original = (WebCallResult)cachedValue; - return original.Cached(); - } - - _logger.CacheNotHit(cacheKey); + _logger.CacheHit(cacheKey); + var original = (WebCallResult)cachedValue; + return original.Cached(); } - int currentTry = 0; - while (true) + _logger.CacheNotHit(cacheKey); + } + + int currentTry = 0; + while (true) + { + currentTry++; + var requestId = ExchangeHelpers.NextId(); + + var prepareResult = await PrepareAsync(requestId, baseAddress, definition, cancellationToken, additionalHeaders, weight, weightSingleLimiter, rateLimitKeySuffix).ConfigureAwait(false); + if (!prepareResult) + return new WebCallResult(prepareResult.Error!); + + var request = CreateRequest( + requestId, + baseAddress, + definition, + uriParameters, + bodyParameters, + additionalHeaders); + _logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"))); + TotalRequestsMade++; + var result = await GetResponseAsync(definition, request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false); + if (result.Error is not CancellationRequestedError) { - currentTry++; - var requestId = ExchangeHelpers.NextId(); + var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]"; + if (!result) + _logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString(), originalData, result.Error?.Exception); + else + _logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), originalData); + } + else + { + _logger.RestApiCancellationRequested(result.RequestId); + } - var prepareResult = await PrepareAsync(requestId, baseAddress, definition, cancellationToken, additionalHeaders, weight, weightSingleLimiter, rateLimitKeySuffix).ConfigureAwait(false); - if (!prepareResult) - return new WebCallResult(prepareResult.Error!); + if (await ShouldRetryRequestAsync(definition.RateLimitGate, result, currentTry).ConfigureAwait(false)) + continue; - var request = CreateRequest( - requestId, - baseAddress, - definition, - uriParameters, - bodyParameters, - additionalHeaders); - _logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"))); - TotalRequestsMade++; - var result = await GetResponseAsync(definition, request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false); - if (result.Error is not CancellationRequestedError) + if (result.Success && + ShouldCache(definition)) + { + _cache.Add(cacheKey!, result); + } + + return result; + } + } + + /// + /// Prepare before sending a request. Sync time between client and server and check rate limits + /// + /// Request id + /// Host and schema + /// Request definition + /// Cancellation token + /// Additional headers for this request + /// Override the request weight for this request + /// Specify the weight to apply to the individual rate limit guard for this request + /// An additional optional suffix for the key selector + /// + /// + protected virtual async Task PrepareAsync( + int requestId, + string baseAddress, + RequestDefinition definition, + CancellationToken cancellationToken, + Dictionary? additionalHeaders = null, + int? weight = null, + int? weightSingleLimiter = null, + string? rateLimitKeySuffix = null) + { + // Time sync + if (definition.Authenticated) + { + if (AuthenticationProvider == null) + { + _logger.RestApiNoApiCredentials(requestId, definition.Path); + return new CallResult(new NoApiCredentialsError()); + } + + var syncTask = SyncTimeAsync(); + var timeSyncInfo = GetTimeSyncInfo(); + + if (timeSyncInfo != null && timeSyncInfo.TimeSyncState.LastSyncTime == default) + { + // Initially with first request we'll need to wait for the time syncing, if it's not the first request we can just continue + var syncTimeResult = await syncTask.ConfigureAwait(false); + if (!syncTimeResult) { - var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]"; - if (!result) - _logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString(), originalData, result.Error?.Exception); - else - _logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), originalData); + _logger.RestApiFailedToSyncTime(requestId, syncTimeResult.Error!.ToString()); + return syncTimeResult.AsDataless(); + } + } + } + + // Rate limiting + var requestWeight = weight ?? definition.Weight; + if (requestWeight != 0) + { + if (definition.RateLimitGate == null) + throw new Exception("Ratelimit gate not set when request weight is not 0"); + + if (ClientOptions.RateLimiterEnabled) + { + var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false); + if (!limitResult) + return new CallResult(limitResult.Error!); + } + } + + // Endpoint specific rate limiting + if (definition.LimitGuard != null && ClientOptions.RateLimiterEnabled) + { + if (definition.RateLimitGate == null) + throw new Exception("Ratelimit gate not set when endpoint limit is specified"); + + if (ClientOptions.RateLimiterEnabled) + { + var singleRequestWeight = weightSingleLimiter ?? 1; + var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, singleRequestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false); + if (!limitResult) + return new CallResult(limitResult.Error!); + } + } + + return CallResult.SuccessResult; + } + + /// + /// Creates a request object + /// + /// Id of the request + /// Host and schema + /// Request definition + /// The query parameters of the request + /// The body parameters of the request + /// Additional headers to send with the request + /// + protected virtual IRequest CreateRequest( + int requestId, + string baseAddress, + RequestDefinition definition, + ParameterCollection? uriParameters, + ParameterCollection? bodyParameters, + Dictionary? additionalHeaders) + { + var requestConfiguration = new RestRequestConfiguration( + definition, + baseAddress, + uriParameters == null ? new Dictionary() : CreateParameterDictionary(uriParameters), + bodyParameters == null ? new Dictionary() : CreateParameterDictionary(bodyParameters), + new Dictionary(additionalHeaders ?? []), + definition.ArraySerialization ?? ArraySerialization, + definition.ParameterPosition ?? ParameterPositions[definition.Method], + definition.RequestBodyFormat ?? RequestBodyFormat); + + try + { + AuthenticationProvider?.ProcessRequest(this, requestConfiguration); + } + catch (Exception ex) + { + throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex); + } + + var queryString = requestConfiguration.GetQueryString(true); + if (!string.IsNullOrEmpty(queryString) && !queryString.StartsWith("?")) + queryString = $"?{queryString}"; + + var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString); + var request = RequestFactory.Create(definition.Method, uri, requestId); + request.Accept = Constants.JsonContentHeader; + + foreach (var header in requestConfiguration.Headers) + request.AddHeader(header.Key, header.Value); + + foreach (var header in StandardRequestHeaders) + { + // Only add it if it isn't overwritten + if (!requestConfiguration.Headers.ContainsKey(header.Key)) + request.AddHeader(header.Key, header.Value); + } + + if (requestConfiguration.ParameterPosition == HttpMethodParameterPosition.InBody) + { + var contentType = requestConfiguration.BodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader; + var bodyContent = requestConfiguration.GetBodyContent(); + if (bodyContent != null) + { + request.SetContent(bodyContent, contentType); + } + else + { + if (requestConfiguration.BodyParameters != null && requestConfiguration.BodyParameters.Count != 0) + WriteParamBody(request, requestConfiguration.BodyParameters, contentType); + else + request.SetContent(RequestBodyEmptyContent, contentType); + } + } + + return request; + } + + /// + /// Executes the request and returns the result deserialized into the type parameter class + /// + /// The request definition + /// The request object to execute + /// The ratelimit gate used + /// Cancellation token + /// + protected virtual async Task> GetResponseAsync( + RequestDefinition requestDefinition, + IRequest request, + IRateLimitGate? gate, + CancellationToken cancellationToken) + { + var sw = Stopwatch.StartNew(); + Stream? responseStream = null; + IResponse? response = null; + IStreamMessageAccessor? accessor = null; + try + { + response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false); + sw.Stop(); + var statusCode = response.StatusCode; + var headers = response.ResponseHeaders; + var responseLength = response.ContentLength; + responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false); + var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData; + + accessor = CreateAccessor(); + if (!response.IsSuccessStatusCode && !requestDefinition.TryParseOnNonSuccess) + { + // Error response + var readResult = await accessor.Read(responseStream, true).ConfigureAwait(false); + + Error error; + if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429) + { + var rateError = ParseRateLimitResponse((int)response.StatusCode, response.ResponseHeaders, accessor); + if (rateError.RetryAfter != null && gate != null && ClientOptions.RateLimiterEnabled) + { + _logger.RestApiRateLimitPauseUntil(request.RequestId, rateError.RetryAfter.Value); + await gate.SetRetryAfterGuardAsync(rateError.RetryAfter.Value).ConfigureAwait(false); + } + + error = rateError; } else { - _logger.RestApiCancellationRequested(result.RequestId); + error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor, readResult.Error?.Exception); } - if (await ShouldRetryRequestAsync(definition.RateLimitGate, result, currentTry).ConfigureAwait(false)) - continue; + if (error.Code == null || error.Code == 0) + error.Code = (int)response.StatusCode; - if (result.Success && - ShouldCache(definition)) - { - _cache.Add(cacheKey!, result); - } - - return result; + return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!); } + + var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false); + if (typeof(T) == typeof(object)) + // Success status code and expected empty response, assume it's correct + return new WebCallResult(statusCode, headers, sw.Elapsed, 0, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]", request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null); + + if (!valid) + { + // Invalid json + return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, valid.Error); + } + + // Json response received + var parsedError = TryParseError(requestDefinition, response.ResponseHeaders, accessor); + if (parsedError != null) + { + if (parsedError is ServerRateLimitError rateError) + { + if (rateError.RetryAfter != null && gate != null && ClientOptions.RateLimiterEnabled) + { + _logger.RestApiRateLimitPauseUntil(request.RequestId, rateError.RetryAfter.Value); + await gate.SetRetryAfterGuardAsync(rateError.RetryAfter.Value).ConfigureAwait(false); + } + } + + // Success status code, but TryParseError determined it was an error response + return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError); + } + + var deserializeResult = accessor.Deserialize(); + return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error); } - - /// - /// Prepare before sending a request. Sync time between client and server and check rate limits - /// - /// Request id - /// Host and schema - /// Request definition - /// Cancellation token - /// Additional headers for this request - /// Override the request weight for this request - /// Specify the weight to apply to the individual rate limit guard for this request - /// An additional optional suffix for the key selector - /// - /// - protected virtual async Task PrepareAsync( - int requestId, - string baseAddress, - RequestDefinition definition, - CancellationToken cancellationToken, - Dictionary? additionalHeaders = null, - int? weight = null, - int? weightSingleLimiter = null, - string? rateLimitKeySuffix = null) + catch (HttpRequestException requestException) { - // Time sync - if (definition.Authenticated) - { - if (AuthenticationProvider == null) - { - _logger.RestApiNoApiCredentials(requestId, definition.Path); - return new CallResult(new NoApiCredentialsError()); - } - - var syncTask = SyncTimeAsync(); - var timeSyncInfo = GetTimeSyncInfo(); - - if (timeSyncInfo != null && timeSyncInfo.TimeSyncState.LastSyncTime == default) - { - // Initially with first request we'll need to wait for the time syncing, if it's not the first request we can just continue - var syncTimeResult = await syncTask.ConfigureAwait(false); - if (!syncTimeResult) - { - _logger.RestApiFailedToSyncTime(requestId, syncTimeResult.Error!.ToString()); - return syncTimeResult.AsDataless(); - } - } - } - - // Rate limiting - var requestWeight = weight ?? definition.Weight; - if (requestWeight != 0) - { - if (definition.RateLimitGate == null) - throw new Exception("Ratelimit gate not set when request weight is not 0"); - - if (ClientOptions.RateLimiterEnabled) - { - var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false); - if (!limitResult) - return new CallResult(limitResult.Error!); - } - } - - // Endpoint specific rate limiting - if (definition.LimitGuard != null && ClientOptions.RateLimiterEnabled) - { - if (definition.RateLimitGate == null) - throw new Exception("Ratelimit gate not set when endpoint limit is specified"); - - if (ClientOptions.RateLimiterEnabled) - { - var singleRequestWeight = weightSingleLimiter ?? 1; - var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, singleRequestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false); - if (!limitResult) - return new CallResult(limitResult.Error!); - } - } - - return CallResult.SuccessResult; + // Request exception, can't reach server for instance + var error = new WebError(requestException.Message, requestException); + return new WebCallResult(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error); } - - /// - /// Creates a request object - /// - /// Id of the request - /// Host and schema - /// Request definition - /// The query parameters of the request - /// The body parameters of the request - /// Additional headers to send with the request - /// - protected virtual IRequest CreateRequest( - int requestId, - string baseAddress, - RequestDefinition definition, - ParameterCollection? uriParameters, - ParameterCollection? bodyParameters, - Dictionary? additionalHeaders) + catch (OperationCanceledException canceledException) { - var requestConfiguration = new RestRequestConfiguration( - definition, - baseAddress, - uriParameters == null ? new Dictionary() : CreateParameterDictionary(uriParameters), - bodyParameters == null ? new Dictionary() : CreateParameterDictionary(bodyParameters), - new Dictionary(additionalHeaders ?? []), - definition.ArraySerialization ?? ArraySerialization, - definition.ParameterPosition ?? ParameterPositions[definition.Method], - definition.RequestBodyFormat ?? RequestBodyFormat); - - try + if (cancellationToken != default && canceledException.CancellationToken == cancellationToken) { - AuthenticationProvider?.ProcessRequest(this, requestConfiguration); + // Cancellation token canceled by caller + return new WebCallResult(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError(canceledException)); } - catch (Exception ex) + else { - throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex); - } - - var queryString = requestConfiguration.GetQueryString(true); - if (!string.IsNullOrEmpty(queryString) && !queryString.StartsWith("?")) - queryString = $"?{queryString}"; - - var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString); - var request = RequestFactory.Create(definition.Method, uri, requestId); - request.Accept = Constants.JsonContentHeader; - - foreach (var header in requestConfiguration.Headers) - request.AddHeader(header.Key, header.Value); - - foreach (var header in StandardRequestHeaders) - { - // Only add it if it isn't overwritten - if (!requestConfiguration.Headers.ContainsKey(header.Key)) - request.AddHeader(header.Key, header.Value); - } - - if (requestConfiguration.ParameterPosition == HttpMethodParameterPosition.InBody) - { - var contentType = requestConfiguration.BodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader; - var bodyContent = requestConfiguration.GetBodyContent(); - if (bodyContent != null) - { - request.SetContent(bodyContent, contentType); - } - else - { - if (requestConfiguration.BodyParameters != null && requestConfiguration.BodyParameters.Count != 0) - WriteParamBody(request, requestConfiguration.BodyParameters, contentType); - else - request.SetContent(RequestBodyEmptyContent, contentType); - } - } - - return request; - } - - /// - /// Executes the request and returns the result deserialized into the type parameter class - /// - /// The request definition - /// The request object to execute - /// The ratelimit gate used - /// Cancellation token - /// - protected virtual async Task> GetResponseAsync( - RequestDefinition requestDefinition, - IRequest request, - IRateLimitGate? gate, - CancellationToken cancellationToken) - { - var sw = Stopwatch.StartNew(); - Stream? responseStream = null; - IResponse? response = null; - IStreamMessageAccessor? accessor = null; - try - { - response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false); - sw.Stop(); - var statusCode = response.StatusCode; - var headers = response.ResponseHeaders; - var responseLength = response.ContentLength; - responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false); - var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData; - - accessor = CreateAccessor(); - if (!response.IsSuccessStatusCode && !requestDefinition.TryParseOnNonSuccess) - { - // Error response - var readResult = await accessor.Read(responseStream, true).ConfigureAwait(false); - - Error error; - if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429) - { - var rateError = ParseRateLimitResponse((int)response.StatusCode, response.ResponseHeaders, accessor); - if (rateError.RetryAfter != null && gate != null && ClientOptions.RateLimiterEnabled) - { - _logger.RestApiRateLimitPauseUntil(request.RequestId, rateError.RetryAfter.Value); - await gate.SetRetryAfterGuardAsync(rateError.RetryAfter.Value).ConfigureAwait(false); - } - - error = rateError; - } - else - { - error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor, readResult.Error?.Exception); - } - - if (error.Code == null || error.Code == 0) - error.Code = (int)response.StatusCode; - - return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!); - } - - var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false); - if (typeof(T) == typeof(object)) - // Success status code and expected empty response, assume it's correct - return new WebCallResult(statusCode, headers, sw.Elapsed, 0, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]", request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null); - - if (!valid) - { - // Invalid json - return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, valid.Error); - } - - // Json response received - var parsedError = TryParseError(requestDefinition, response.ResponseHeaders, accessor); - if (parsedError != null) - { - if (parsedError is ServerRateLimitError rateError) - { - if (rateError.RetryAfter != null && gate != null && ClientOptions.RateLimiterEnabled) - { - _logger.RestApiRateLimitPauseUntil(request.RequestId, rateError.RetryAfter.Value); - await gate.SetRetryAfterGuardAsync(rateError.RetryAfter.Value).ConfigureAwait(false); - } - } - - // Success status code, but TryParseError determined it was an error response - return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError); - } - - var deserializeResult = accessor.Deserialize(); - return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error); - } - catch (HttpRequestException requestException) - { - // Request exception, can't reach server for instance - var error = new WebError(requestException.Message, requestException); + // Request timed out + var error = new WebError($"Request timed out", exception: canceledException); + error.ErrorType = ErrorType.Timeout; return new WebCallResult(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error); } - catch (OperationCanceledException canceledException) - { - if (cancellationToken != default && canceledException.CancellationToken == cancellationToken) - { - // Cancellation token canceled by caller - return new WebCallResult(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError(canceledException)); - } - else - { - // Request timed out - var error = new WebError($"Request timed out", exception: canceledException); - error.ErrorType = ErrorType.Timeout; - return new WebCallResult(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error); - } - } - finally - { - accessor?.Clear(); - responseStream?.Close(); - response?.Close(); - } } - - /// - /// Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error. - /// This method will be called for each response to be able to check if the response is an error or not. - /// If the response is an error this method should return the parsed error, else it should return null - /// - /// Request definition - /// Data accessor - /// The response headers - /// Null if not an error, Error otherwise - protected virtual Error? TryParseError(RequestDefinition requestDefinition, KeyValuePair[] responseHeaders, IMessageAccessor accessor) => null; - - /// - /// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever. - /// Note that this is always called; even when the request might be successful - /// - /// WebCallResult type parameter - /// The rate limit gate the call used - /// The result of the call - /// The current try number - /// True if call should retry, false if the call should return - protected virtual async Task ShouldRetryRequestAsync(IRateLimitGate? gate, WebCallResult callResult, int tries) + finally { - if (tries >= 2) - // Only retry once + accessor?.Clear(); + responseStream?.Close(); + response?.Close(); + } + } + + /// + /// Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error. + /// This method will be called for each response to be able to check if the response is an error or not. + /// If the response is an error this method should return the parsed error, else it should return null + /// + /// Request definition + /// Data accessor + /// The response headers + /// Null if not an error, Error otherwise + protected virtual Error? TryParseError(RequestDefinition requestDefinition, KeyValuePair[] responseHeaders, IMessageAccessor accessor) => null; + + /// + /// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever. + /// Note that this is always called; even when the request might be successful + /// + /// WebCallResult type parameter + /// The rate limit gate the call used + /// The result of the call + /// The current try number + /// True if call should retry, false if the call should return + protected virtual async Task ShouldRetryRequestAsync(IRateLimitGate? gate, WebCallResult callResult, int tries) + { + if (tries >= 2) + // Only retry once + return false; + + if (callResult.Error is ServerRateLimitError + && ClientOptions.RateLimiterEnabled + && ClientOptions.RateLimitingBehaviour != RateLimitingBehaviour.Fail + && gate != null) + { + var retryTime = await gate.GetRetryAfterTime().ConfigureAwait(false); + if (retryTime == null) return false; - if (callResult.Error is ServerRateLimitError - && ClientOptions.RateLimiterEnabled - && ClientOptions.RateLimitingBehaviour != RateLimitingBehaviour.Fail - && gate != null) + if (retryTime.Value - DateTime.UtcNow < TimeSpan.FromSeconds(60)) { - var retryTime = await gate.GetRetryAfterTime().ConfigureAwait(false); - if (retryTime == null) - return false; - - if (retryTime.Value - DateTime.UtcNow < TimeSpan.FromSeconds(60)) - { - _logger.RestApiRateLimitRetry(callResult.RequestId!.Value, retryTime.Value); - return true; - } - } - - return false; - } - - /// - /// Writes the parameters of the request to the request object body - /// - /// The request to set the parameters on - /// The parameters to set - /// The content type of the data - protected virtual void WriteParamBody(IRequest request, IDictionary parameters, string contentType) - { - if (contentType == Constants.JsonContentHeader) - { - var serializer = CreateSerializer(); - if (serializer is not IStringMessageSerializer stringSerializer) - throw new InvalidOperationException("Non-string message serializer can't get serialized request body"); - - // Write the parameters as json in the body - string stringData; - if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value)) - stringData = stringSerializer.Serialize(value); - else - stringData = stringSerializer.Serialize(parameters); - request.SetContent(stringData, contentType); - } - else if (contentType == Constants.FormContentHeader) - { - // Write the parameters as form data in the body - var stringData = parameters.ToFormData(); - request.SetContent(stringData, contentType); + _logger.RestApiRateLimitRetry(callResult.RequestId!.Value, retryTime.Value); + return true; } } - /// - /// Parse an error response from the server. Only used when server returns a status other than Success(200) or ratelimit error (429 or 418) - /// - /// The response status code - /// The response headers - /// Data accessor - /// Exception - /// - protected virtual Error ParseErrorResponse(int httpStatusCode, KeyValuePair[] responseHeaders, IMessageAccessor accessor, Exception? exception) + return false; + } + + /// + /// Writes the parameters of the request to the request object body + /// + /// The request to set the parameters on + /// The parameters to set + /// The content type of the data + protected virtual void WriteParamBody(IRequest request, IDictionary parameters, string contentType) + { + if (contentType == Constants.JsonContentHeader) { - return new ServerError(ErrorInfo.Unknown, exception); + var serializer = CreateSerializer(); + if (serializer is not IStringMessageSerializer stringSerializer) + throw new InvalidOperationException("Non-string message serializer can't get serialized request body"); + + // Write the parameters as json in the body + string stringData; + if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value)) + stringData = stringSerializer.Serialize(value); + else + stringData = stringSerializer.Serialize(parameters); + request.SetContent(stringData, contentType); } - - /// - /// Parse a rate limit error response from the server. Only used when server returns http status 429 or 418 - /// - /// The response status code - /// The response headers - /// Data accessor - /// - protected virtual ServerRateLimitError ParseRateLimitResponse(int httpStatusCode, KeyValuePair[] responseHeaders, IMessageAccessor accessor) + else if (contentType == Constants.FormContentHeader) { - // Handle retry after header - var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase)); - if (retryAfterHeader.Value?.Any() != true) - return new ServerRateLimitError(); + // Write the parameters as form data in the body + var stringData = parameters.ToFormData(); + request.SetContent(stringData, contentType); + } + } - var value = retryAfterHeader.Value.First(); - if (int.TryParse(value, out var seconds)) - return new ServerRateLimitError() { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) }; - - if (DateTime.TryParse(value, out var datetime)) - return new ServerRateLimitError() { RetryAfter = datetime }; + /// + /// Parse an error response from the server. Only used when server returns a status other than Success(200) or ratelimit error (429 or 418) + /// + /// The response status code + /// The response headers + /// Data accessor + /// Exception + /// + protected virtual Error ParseErrorResponse(int httpStatusCode, KeyValuePair[] responseHeaders, IMessageAccessor accessor, Exception? exception) + { + return new ServerError(ErrorInfo.Unknown, exception); + } + /// + /// Parse a rate limit error response from the server. Only used when server returns http status 429 or 418 + /// + /// The response status code + /// The response headers + /// Data accessor + /// + protected virtual ServerRateLimitError ParseRateLimitResponse(int httpStatusCode, KeyValuePair[] responseHeaders, IMessageAccessor accessor) + { + // Handle retry after header + var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase)); + if (!(retryAfterHeader.Value.Length > 0)) return new ServerRateLimitError(); - } - /// - /// Create the parameter IDictionary - /// - /// - /// - protected internal IDictionary CreateParameterDictionary(IDictionary parameters) + var value = retryAfterHeader.Value.First(); + if (int.TryParse(value, out var seconds)) + return new ServerRateLimitError() { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) }; + + if (DateTime.TryParse(value, out var datetime)) + return new ServerRateLimitError() { RetryAfter = datetime }; + + return new ServerRateLimitError(); + } + + /// + /// Create the parameter IDictionary + /// + /// + /// + protected internal IDictionary CreateParameterDictionary(IDictionary parameters) + { + if (!OrderParameters) + return parameters; + + return new SortedDictionary(parameters, ParameterOrderComparer); + } + + /// + /// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues + /// + /// 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(); + if (timeSyncParams == null) + return new WebCallResult(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null); + + if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false)) { - if (!OrderParameters) - return parameters; - - return new SortedDictionary(parameters, ParameterOrderComparer); - } - - /// - /// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues - /// - /// 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(); - if (timeSyncParams == null) - return new WebCallResult(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null); - - if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false)) + if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval) { - if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval) - { - timeSyncParams.TimeSyncState.Semaphore.Release(); - return new WebCallResult(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null); - } + timeSyncParams.TimeSyncState.Semaphore.Release(); + return new WebCallResult(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null); + } - var localTime = DateTime.UtcNow; - var result = await GetServerTimestampAsync().ConfigureAwait(false); + var localTime = DateTime.UtcNow; + var result = await GetServerTimestampAsync().ConfigureAwait(false); + if (!result) + { + timeSyncParams.TimeSyncState.Semaphore.Release(); + return result.As(false); + } + + if (TotalRequestsMade == 1) + { + // If this was the first request make another one to calculate the offset since the first one can be slower + localTime = DateTime.UtcNow; + result = await GetServerTimestampAsync().ConfigureAwait(false); if (!result) { timeSyncParams.TimeSyncState.Semaphore.Release(); return result.As(false); } - - if (TotalRequestsMade == 1) - { - // If this was the first request make another one to calculate the offset since the first one can be slower - localTime = DateTime.UtcNow; - result = await GetServerTimestampAsync().ConfigureAwait(false); - if (!result) - { - timeSyncParams.TimeSyncState.Semaphore.Release(); - return result.As(false); - } - } - - // Calculate time offset between local and server - var offset = result.Data - localTime.AddMilliseconds(result.ResponseTime!.Value.TotalMilliseconds / 2); - timeSyncParams.UpdateTimeOffset(offset); - timeSyncParams.TimeSyncState.Semaphore.Release(); } - return new WebCallResult(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null); + // Calculate time offset between local and server + var offset = result.Data - localTime.AddMilliseconds(result.ResponseTime!.Value.TotalMilliseconds / 2); + timeSyncParams.UpdateTimeOffset(offset); + timeSyncParams.TimeSyncState.Semaphore.Release(); } - private bool ShouldCache(RequestDefinition definition) - => ClientOptions.CachingEnabled - && definition.Method == HttpMethod.Get - && !definition.PreventCaching; + return new WebCallResult(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null); } + + private bool ShouldCache(RequestDefinition definition) + => ClientOptions.CachingEnabled + && definition.Method == HttpMethod.Get + && !definition.PreventCaching; } diff --git a/CryptoExchange.Net/Clients/SocketApiClient.cs b/CryptoExchange.Net/Clients/SocketApiClient.cs index fcbfbe9..617209f 100644 --- a/CryptoExchange.Net/Clients/SocketApiClient.cs +++ b/CryptoExchange.Net/Clients/SocketApiClient.cs @@ -839,8 +839,11 @@ public abstract class SocketApiClient : BaseApiClient, ISocketApiClient /// /// Dispose the client /// - public override void Dispose() + public override void Dispose(bool disposing) { + if (disposing) + return; + _disposing = true; var tasks = new List(); { @@ -855,7 +858,7 @@ public abstract class SocketApiClient : BaseApiClient, ISocketApiClient } semaphoreSlim?.Dispose(); - base.Dispose(); + base.Dispose(disposing); } /// diff --git a/CryptoExchange.Net/Objects/AsyncAutoResetEvent.cs b/CryptoExchange.Net/Objects/AsyncAutoResetEvent.cs index 96ecc7c..48b6dbc 100644 --- a/CryptoExchange.Net/Objects/AsyncAutoResetEvent.cs +++ b/CryptoExchange.Net/Objects/AsyncAutoResetEvent.cs @@ -118,6 +118,18 @@ public class AsyncResetEvent : IDisposable /// public void Dispose() { - _waits.Clear(); + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Dispose + /// + public void Dispose(bool disposing) + { + if (disposing) + { + _waits.Clear(); + } } } diff --git a/CryptoExchange.Net/Objects/RequestDefinition.cs b/CryptoExchange.Net/Objects/RequestDefinition.cs index 429100d..cd6caa2 100644 --- a/CryptoExchange.Net/Objects/RequestDefinition.cs +++ b/CryptoExchange.Net/Objects/RequestDefinition.cs @@ -1,95 +1,94 @@ -using CryptoExchange.Net.RateLimiting.Interfaces; +using CryptoExchange.Net.RateLimiting.Interfaces; using System.Net.Http; -namespace CryptoExchange.Net.Objects +namespace CryptoExchange.Net.Objects; + +/// +/// The definition of a rest request +/// +public class RequestDefinition { + private string? _stringRep; + + // Basics + /// - /// The definition of a rest request + /// Path of the request /// - public class RequestDefinition + public string Path { get; set; } + /// + /// Http method of the request + /// + public HttpMethod Method { get; set; } + /// + /// Is the request authenticated + /// + public bool Authenticated { get; set; } + + // Formatting + + /// + /// The body format for this request + /// + public RequestBodyFormat? RequestBodyFormat { get; set; } + /// + /// The position of parameters for this request + /// + public HttpMethodParameterPosition? ParameterPosition { get; set; } + /// + /// The array serialization type for this request + /// + public ArrayParametersSerialization? ArraySerialization { get; set; } + + // Rate limiting + + /// + /// Request weight + /// + public int Weight { get; set; } = 1; + + /// + /// Rate limit gate to use + /// + public IRateLimitGate? RateLimitGate { get; set; } + + /// + /// Individual endpoint rate limit guard to use + /// + public IRateLimitGuard? LimitGuard { get; set; } + + /// + /// Whether this request should never be cached + /// + public bool PreventCaching { get; set; } + + /// + /// Whether the response to this requests should attempted to be parsed even when the status indicates failure + /// + public bool TryParseOnNonSuccess { get; set; } + + /// + /// Connection id + /// + public int? ConnectionId { get; set; } + + /// + /// ctor + /// + /// + /// + public RequestDefinition(string path, HttpMethod method) { - private string? _stringRep; + Path = path; + Method = method; - // Basics + if (!Path.StartsWith("/")) + Path = $"/{Path}"; + } - /// - /// Path of the request - /// - public string Path { get; set; } - /// - /// Http method of the request - /// - public HttpMethod Method { get; set; } - /// - /// Is the request authenticated - /// - public bool Authenticated { get; set; } - - // Formatting - - /// - /// The body format for this request - /// - public RequestBodyFormat? RequestBodyFormat { get; set; } - /// - /// The position of parameters for this request - /// - public HttpMethodParameterPosition? ParameterPosition { get; set; } - /// - /// The array serialization type for this request - /// - public ArrayParametersSerialization? ArraySerialization { get; set; } - - // Rate limiting - - /// - /// Request weight - /// - public int Weight { get; set; } = 1; - - /// - /// Rate limit gate to use - /// - public IRateLimitGate? RateLimitGate { get; set; } - - /// - /// Individual endpoint rate limit guard to use - /// - public IRateLimitGuard? LimitGuard { get; set; } - - /// - /// Whether this request should never be cached - /// - public bool PreventCaching { get; set; } - - /// - /// Whether the response to this requests should attempted to be parsed even when the status indicates failure - /// - public bool TryParseOnNonSuccess { get; set; } - - /// - /// Connection id - /// - public int? ConnectionId { get; set; } - - /// - /// ctor - /// - /// - /// - public RequestDefinition(string path, HttpMethod method) - { - Path = path; - Method = method; - - if (!Path.StartsWith("/")) - Path = $"/{Path}"; - } - - /// - public override string ToString() - { - return _stringRep ??= $"{Method} {Path}{(Authenticated ? " authenticated" : "")}"; - } + /// + public override string ToString() + { + return _stringRep ??= $"{Method} {Path}{(Authenticated ? " authenticated" : "")}"; } } diff --git a/CryptoExchange.Net/Objects/RequestDefinitionCache.cs b/CryptoExchange.Net/Objects/RequestDefinitionCache.cs index 1fac34b..cd73085 100644 --- a/CryptoExchange.Net/Objects/RequestDefinitionCache.cs +++ b/CryptoExchange.Net/Objects/RequestDefinitionCache.cs @@ -1,116 +1,115 @@ -using CryptoExchange.Net.RateLimiting.Interfaces; +using CryptoExchange.Net.RateLimiting.Interfaces; using System.Collections.Concurrent; using System.Net.Http; -namespace CryptoExchange.Net.Objects +namespace CryptoExchange.Net.Objects; + +/// +/// Request definitions cache +/// +public class RequestDefinitionCache { + private readonly ConcurrentDictionary _definitions = new(); + /// - /// Request definitions cache + /// Get a definition if it is already in the cache or create a new definition and add it to the cache /// - public class RequestDefinitionCache + /// The HttpMethod + /// Endpoint path + /// Endpoint is authenticated + /// + public RequestDefinition GetOrCreate(HttpMethod method, string path, bool authenticated = false) + => GetOrCreate(method, path, null, 0, authenticated, null, null, null, null, null); + + /// + /// Get a definition if it is already in the cache or create a new definition and add it to the cache + /// + /// The HttpMethod + /// Endpoint path + /// The rate limit gate + /// Request weight + /// Endpoint is authenticated + /// + public RequestDefinition GetOrCreate(HttpMethod method, string path, IRateLimitGate rateLimitGate, int weight = 1, bool authenticated = false) + => GetOrCreate(method, path, rateLimitGate, weight, authenticated, null, null, null, null, null); + + /// + /// Get a definition if it is already in the cache or create a new definition and add it to the cache + /// + /// The HttpMethod + /// Endpoint path + /// The rate limit gate + /// The rate limit guard for this specific endpoint + /// Request weight + /// Endpoint is authenticated + /// Request body format + /// Parameter position + /// Array serialization type + /// Prevent request caching + /// Try parse the response even when status is not success + /// + public RequestDefinition GetOrCreate( + HttpMethod method, + string path, + IRateLimitGate? rateLimitGate, + int weight, + bool authenticated, + IRateLimitGuard? limitGuard = null, + RequestBodyFormat? requestBodyFormat = null, + HttpMethodParameterPosition? parameterPosition = null, + ArrayParametersSerialization? arraySerialization = null, + bool? preventCaching = null, + bool? tryParseOnNonSuccess = null) + => GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching, tryParseOnNonSuccess); + + /// + /// Get a definition if it is already in the cache or create a new definition and add it to the cache + /// + /// Request identifier + /// The HttpMethod + /// Endpoint path + /// The rate limit gate + /// The rate limit guard for this specific endpoint + /// Request weight + /// Endpoint is authenticated + /// Request body format + /// Parameter position + /// Array serialization type + /// Prevent request caching + /// Try parse the response even when status is not success + /// + public RequestDefinition GetOrCreate( + string identifier, + HttpMethod method, + string path, + IRateLimitGate? rateLimitGate, + int weight, + bool authenticated, + IRateLimitGuard? limitGuard = null, + RequestBodyFormat? requestBodyFormat = null, + HttpMethodParameterPosition? parameterPosition = null, + ArrayParametersSerialization? arraySerialization = null, + bool? preventCaching = null, + bool? tryParseOnNonSuccess = null) { - private readonly ConcurrentDictionary _definitions = new(); - /// - /// Get a definition if it is already in the cache or create a new definition and add it to the cache - /// - /// The HttpMethod - /// Endpoint path - /// Endpoint is authenticated - /// - public RequestDefinition GetOrCreate(HttpMethod method, string path, bool authenticated = false) - => GetOrCreate(method, path, null, 0, authenticated, null, null, null, null, null); - - /// - /// Get a definition if it is already in the cache or create a new definition and add it to the cache - /// - /// The HttpMethod - /// Endpoint path - /// The rate limit gate - /// Request weight - /// Endpoint is authenticated - /// - public RequestDefinition GetOrCreate(HttpMethod method, string path, IRateLimitGate rateLimitGate, int weight = 1, bool authenticated = false) - => GetOrCreate(method, path, rateLimitGate, weight, authenticated, null, null, null, null, null); - - /// - /// Get a definition if it is already in the cache or create a new definition and add it to the cache - /// - /// The HttpMethod - /// Endpoint path - /// The rate limit gate - /// The rate limit guard for this specific endpoint - /// Request weight - /// Endpoint is authenticated - /// Request body format - /// Parameter position - /// Array serialization type - /// Prevent request caching - /// Try parse the response even when status is not success - /// - public RequestDefinition GetOrCreate( - HttpMethod method, - string path, - IRateLimitGate? rateLimitGate, - int weight, - bool authenticated, - IRateLimitGuard? limitGuard = null, - RequestBodyFormat? requestBodyFormat = null, - HttpMethodParameterPosition? parameterPosition = null, - ArrayParametersSerialization? arraySerialization = null, - bool? preventCaching = null, - bool? tryParseOnNonSuccess = null) - => GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching, tryParseOnNonSuccess); - - /// - /// Get a definition if it is already in the cache or create a new definition and add it to the cache - /// - /// Request identifier - /// The HttpMethod - /// Endpoint path - /// The rate limit gate - /// The rate limit guard for this specific endpoint - /// Request weight - /// Endpoint is authenticated - /// Request body format - /// Parameter position - /// Array serialization type - /// Prevent request caching - /// Try parse the response even when status is not success - /// - public RequestDefinition GetOrCreate( - string identifier, - HttpMethod method, - string path, - IRateLimitGate? rateLimitGate, - int weight, - bool authenticated, - IRateLimitGuard? limitGuard = null, - RequestBodyFormat? requestBodyFormat = null, - HttpMethodParameterPosition? parameterPosition = null, - ArrayParametersSerialization? arraySerialization = null, - bool? preventCaching = null, - bool? tryParseOnNonSuccess = null) + if (!_definitions.TryGetValue(identifier, out var def)) { - - if (!_definitions.TryGetValue(identifier, out var def)) + def = new RequestDefinition(path, method) { - def = new RequestDefinition(path, method) - { - Authenticated = authenticated, - LimitGuard = limitGuard, - RateLimitGate = rateLimitGate, - Weight = weight, - ArraySerialization = arraySerialization, - RequestBodyFormat = requestBodyFormat, - ParameterPosition = parameterPosition, - PreventCaching = preventCaching ?? false, - TryParseOnNonSuccess = tryParseOnNonSuccess ?? false - }; - _definitions.TryAdd(identifier, def); - } - - return def; + Authenticated = authenticated, + LimitGuard = limitGuard, + RateLimitGate = rateLimitGate, + Weight = weight, + ArraySerialization = arraySerialization, + RequestBodyFormat = requestBodyFormat, + ParameterPosition = parameterPosition, + PreventCaching = preventCaching ?? false, + TryParseOnNonSuccess = tryParseOnNonSuccess ?? false + }; + _definitions.TryAdd(identifier, def); } + + return def; } } diff --git a/CryptoExchange.Net/Objects/TraceLogger.cs b/CryptoExchange.Net/Objects/TraceLogger.cs index 2617e97..14cec02 100644 --- a/CryptoExchange.Net/Objects/TraceLogger.cs +++ b/CryptoExchange.Net/Objects/TraceLogger.cs @@ -22,8 +22,22 @@ public class TraceLoggerProvider : ILoggerProvider /// public ILogger CreateLogger(string categoryName) => new TraceLogger(categoryName, _logLevel); - /// - public void Dispose() { } + + /// + /// Dispose + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Dispose + /// + public static void Dispose(bool disposing) + { + } } /// diff --git a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs index c70882a..df02a28 100644 --- a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs +++ b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs @@ -578,21 +578,24 @@ public abstract class SymbolOrderBook : ISymbolOrderBook, IDisposable /// protected virtual void Dispose(bool disposing) { - Status = OrderBookStatus.Disposing; + if (disposing) + { + Status = OrderBookStatus.Disposing; - _cts?.Cancel(); - _queueEvent.Set(); + _cts?.Cancel(); + _queueEvent.Set(); - // Clear queue - while (_processQueue.TryDequeue(out _)) { } + // Clear queue + while (_processQueue.TryDequeue(out _)) { } - _processBuffer.Clear(); - _asks.Clear(); - _bids.Clear(); - AskCount = 0; - BidCount = 0; + _processBuffer.Clear(); + _asks.Clear(); + _bids.Clear(); + AskCount = 0; + BidCount = 0; - Status = OrderBookStatus.Disposed; + Status = OrderBookStatus.Disposed; + } } /// diff --git a/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs b/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs index 6a403f0..716f188 100644 --- a/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs +++ b/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs @@ -488,9 +488,18 @@ public class CryptoExchangeWebSocketClient : IWebsocket } /// - /// Dispose the socket + /// Dispose /// public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Dispose the socket + /// + public void Dispose(bool disposing) { if (_disposed) return; diff --git a/CryptoExchange.Net/Sockets/Query.cs b/CryptoExchange.Net/Sockets/Query.cs index e9393b1..9f64025 100644 --- a/CryptoExchange.Net/Sockets/Query.cs +++ b/CryptoExchange.Net/Sockets/Query.cs @@ -169,7 +169,6 @@ public abstract class Query : IMessageProcessor, IDisposable { if (disposing) { - // TODO: dispose managed state (managed objects) _cts?.Dispose(); _event.Dispose(); } diff --git a/CryptoExchange.Net/Sockets/SocketConnection.cs b/CryptoExchange.Net/Sockets/SocketConnection.cs index 91a3cd2..7d4326d 100644 --- a/CryptoExchange.Net/Sockets/SocketConnection.cs +++ b/CryptoExchange.Net/Sockets/SocketConnection.cs @@ -709,14 +709,29 @@ public class SocketConnection : IDisposable } /// - /// Dispose the connection + /// Dispose + /// + protected virtual void Dispose(bool disposing) + { + if (Status != SocketStatus.Disposed) + { + if (disposing) + { + Status = SocketStatus.Disposed; + periodicEvent?.Set(); + periodicEvent?.Dispose(); + _socket.Dispose(); + } + } + } + + /// + /// Dispose /// public void Dispose() { - Status = SocketStatus.Disposed; - periodicEvent?.Set(); - periodicEvent?.Dispose(); - _socket.Dispose(); + Dispose(disposing: true); + GC.SuppressFinalize(this); } /// diff --git a/CryptoExchange.Net/Testing/RestRequestValidator.cs b/CryptoExchange.Net/Testing/RestRequestValidator.cs index b998ae1..f6816c8 100644 --- a/CryptoExchange.Net/Testing/RestRequestValidator.cs +++ b/CryptoExchange.Net/Testing/RestRequestValidator.cs @@ -23,6 +23,8 @@ public class RestRequestValidator where TClient : BaseRestClient private readonly string _baseAddress; private readonly string? _nestedPropertyForCompare; + private static readonly char[] _paramSeparator = new char[] { '?' }; + /// /// ctor /// @@ -117,8 +119,8 @@ public class RestRequestValidator where TClient : BaseRestClient throw new Exception(name + $" authentication not matched. Expected: {expectedAuth}, Actual: {_isAuthenticated(result.AsDataless())}"); if (result.RequestMethod != new HttpMethod(expectedMethod!)) throw new Exception(name + $" http method not matched. Expected {expectedMethod}, Actual: {result.RequestMethod}"); - if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]) - throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]}"); + if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(_paramSeparator)[0]) + throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(_paramSeparator)[0]}"); if (!skipResponseValidation) { @@ -176,8 +178,8 @@ public class RestRequestValidator where TClient : BaseRestClient throw new Exception(name + $" authentication not matched. Expected: {expectedAuth}, Actual: {_isAuthenticated(result)}"); if (result.RequestMethod != new HttpMethod(expectedMethod!)) throw new Exception(name + $" http method not matched. Expected {expectedMethod}, Actual: {result.RequestMethod}"); - if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]) - throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]}"); + if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(_paramSeparator)[0]) + throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(_paramSeparator)[0]}"); Trace.Listeners.Remove(listener); }