diff --git a/CryptoExchange.Net.UnitTests/SocketClientTests.cs b/CryptoExchange.Net.UnitTests/SocketClientTests.cs index 2b9ec6a..bacb863 100644 --- a/CryptoExchange.Net.UnitTests/SocketClientTests.cs +++ b/CryptoExchange.Net.UnitTests/SocketClientTests.cs @@ -26,7 +26,7 @@ namespace CryptoExchange.Net.UnitTests //assert Assert.IsTrue(client.BaseAddress == "http://test.address.com/"); - Assert.IsTrue(client.ReconnectInterval.TotalSeconds == 6); + Assert.IsTrue(client.ClientOptions.ReconnectInterval.TotalSeconds == 6); } [TestCase(true)] diff --git a/CryptoExchange.Net/BaseClient.cs b/CryptoExchange.Net/BaseClient.cs index 7cd8d3a..aa4a652 100644 --- a/CryptoExchange.Net/BaseClient.cs +++ b/CryptoExchange.Net/BaseClient.cs @@ -21,35 +21,19 @@ namespace CryptoExchange.Net /// public abstract class BaseClient : IDisposable { - /// - /// The address of the client - /// - public string BaseAddress { get; } /// /// The name of the exchange the client is for /// - public string ExchangeName { get; } + internal string ExchangeName { get; } /// /// The log object /// protected internal Log log; /// - /// The api proxy - /// - protected ApiProxy? apiProxy; - /// /// The authentication provider /// protected internal AuthenticationProvider? authProvider; /// - /// Should check objects for missing properties based on the model and the received JSON - /// - public bool ShouldCheckObjects { get; set; } - /// - /// If true, the CallResult and DataEvent objects should also contain the originally received json data in the OriginalDaa property - /// - public bool OutputOriginalData { get; private set; } - /// /// The last used id, use NextId() to get the next id and up this /// protected static int lastId; @@ -68,9 +52,9 @@ namespace CryptoExchange.Net }); /// - /// Last id used + /// Provided client options /// - public static int LastId => lastId; + public ClientOptions ClientOptions { get; } /// /// ctor @@ -85,13 +69,12 @@ namespace CryptoExchange.Net log.UpdateWriters(options.LogWriters); log.Level = options.LogLevel; + ClientOptions = options; + ExchangeName = exchangeName; - OutputOriginalData = options.OutputOriginalData; - BaseAddress = options.BaseAddress; - apiProxy = options.Proxy; + //BaseAddress = options.BaseAddress; log.Write(LogLevel.Debug, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {ExchangeName}.Net: v{GetType().Assembly.GetName().Version}"); - ShouldCheckObjects = options.ShouldCheckObjects; } /// @@ -145,11 +128,10 @@ namespace CryptoExchange.Net /// /// The type to deserialize into /// The data to deserialize - /// Whether or not the parsing should be checked for missing properties (will output data to the logging if log verbosity is Debug) /// A specific serializer to use /// Id of the request the data is returned from (used for grouping logging by request) /// - protected CallResult Deserialize(string data, bool? checkObject = null, JsonSerializer? serializer = null, int? requestId = null) + protected CallResult Deserialize(string data, JsonSerializer? serializer = null, int? requestId = null) { var tokenResult = ValidateJson(data); if (!tokenResult) @@ -158,7 +140,7 @@ namespace CryptoExchange.Net return new CallResult(default, tokenResult.Error); } - return Deserialize(tokenResult.Data, checkObject, serializer, requestId); + return Deserialize(tokenResult.Data, serializer, requestId); } /// @@ -166,39 +148,16 @@ namespace CryptoExchange.Net /// /// The type to deserialize into /// The data to deserialize - /// Whether or not the parsing should be checked for missing properties (will output data to the logging if log verbosity is Debug) /// A specific serializer to use /// Id of the request the data is returned from (used for grouping logging by request) /// - protected CallResult Deserialize(JToken obj, bool? checkObject = null, JsonSerializer? serializer = null, int? requestId = null) + protected CallResult Deserialize(JToken obj, JsonSerializer? serializer = null, int? requestId = null) { if (serializer == null) serializer = defaultSerializer; try { - if ((checkObject ?? ShouldCheckObjects)&& log.Level <= LogLevel.Debug) - { - // This checks the input JToken object against the class it is being serialized into and outputs any missing fields - // in either the input or the class - try - { - if (obj is JObject o) - { - CheckObject(typeof(T), o, requestId); - } - else if (obj is JArray j) - { - if (j.HasValues && j[0] is JObject jObject) - CheckObject(typeof(T).GetElementType(), jObject, requestId); - } - } - catch (Exception e) - { - log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{ requestId}] " : "")}Failed to check response data: " + (e.InnerException?.Message ?? e.Message)); - } - } - return new CallResult(obj.ToObject(serializer), null); } catch (JsonReaderException jre) @@ -243,12 +202,12 @@ namespace CryptoExchange.Net // If we have to output the original json data or output the data into the logging we'll have to read to full response // in order to log/return the json data - if (OutputOriginalData || log.Level <= LogLevel.Debug) + if (ClientOptions.OutputOriginalData || log.Level <= LogLevel.Debug) { var data = await reader.ReadToEndAsync().ConfigureAwait(false); log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] ": "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms: {data}"); - var result = Deserialize(data, null, serializer, requestId); - if(OutputOriginalData) + var result = Deserialize(data, serializer, requestId); + if(ClientOptions.OutputOriginalData) result.OriginalData = data; return result; } @@ -308,116 +267,6 @@ namespace CryptoExchange.Net return await reader.ReadToEndAsync().ConfigureAwait(false); } - private void CheckObject(Type type, JObject obj, int? requestId = null) - { - if (type == null) - return; - - if (type.GetCustomAttribute(true) != null) - // If type has a custom JsonConverter we assume this will handle property mapping - return; - - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) - return; - - if (!obj.HasValues && type != typeof(object)) - { - log.Write(LogLevel.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Expected `{type.Name}`, but received object was empty"); - return; - } - - var isDif = false; - var properties = new List(); - var props = type.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy); - foreach (var prop in props) - { - var attr = prop.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault(); - var ignore = prop.GetCustomAttributes(typeof(JsonIgnoreAttribute), false).FirstOrDefault(); - if (ignore != null) - continue; - - var propertyName = ((JsonPropertyAttribute?) attr)?.PropertyName; - properties.Add(propertyName ?? prop.Name); - } - foreach (var token in obj) - { - var d = properties.FirstOrDefault(p => p == token.Key); - if (d == null) - { - d = properties.SingleOrDefault(p => string.Equals(p, token.Key, StringComparison.CurrentCultureIgnoreCase)); - if (d == null) - { - if (!(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))) - { - log.Write(LogLevel.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Local object doesn't have property `{token.Key}` expected in type `{type.Name}`"); - isDif = true; - } - continue; - } - } - - properties.Remove(d); - - var propType = GetProperty(d, props)?.PropertyType; - if (propType == null || token.Value == null) - continue; - if (!IsSimple(propType) && propType != typeof(DateTime)) - { - if (propType.IsArray && token.Value.HasValues && ((JArray)token.Value).Any() && ((JArray)token.Value)[0] is JObject) - CheckObject(propType.GetElementType()!, (JObject)token.Value[0]!, requestId); - else if (token.Value is JObject o) - CheckObject(propType, o, requestId); - } - } - - foreach (var prop in properties) - { - var propInfo = props.First(p => p.Name == prop || - ((JsonPropertyAttribute)p.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault())?.PropertyName == prop); - var optional = propInfo.GetCustomAttributes(typeof(JsonOptionalPropertyAttribute), false).FirstOrDefault(); - if (optional != null) - continue; - - isDif = true; - log.Write(LogLevel.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Local object has property `{prop}` but was not found in received object of type `{type.Name}`"); - } - - if (isDif) - log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{ requestId}] " : "")}Returned data: " + obj); - } - - private static PropertyInfo? GetProperty(string name, IEnumerable props) - { - foreach (var prop in props) - { - var attr = prop.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault(); - if (attr == null) - { - if (string.Equals(prop.Name, name, StringComparison.CurrentCultureIgnoreCase)) - return prop; - } - else - { - if (((JsonPropertyAttribute)attr).PropertyName == name) - return prop; - } - } - return null; - } - - private static bool IsSimple(Type type) - { - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - // nullable type, check if the nested type is simple. - return IsSimple(type.GetGenericArguments()[0]); - } - return type.IsPrimitive - || type.IsEnum - || type == typeof(string) - || type == typeof(decimal); - } - /// /// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique across different client instances /// diff --git a/CryptoExchange.Net/CryptoExchange.Net.xml b/CryptoExchange.Net/CryptoExchange.Net.xml index 2100834..e3846fd 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.xml +++ b/CryptoExchange.Net/CryptoExchange.Net.xml @@ -204,11 +204,6 @@ The base for all clients, websocket client and rest client - - - The address of the client - - The name of the exchange the client is for @@ -219,26 +214,11 @@ The log object - - - The api proxy - - The authentication provider - - - Should check objects for missing properties based on the model and the received JSON - - - - - If true, the CallResult and DataEvent objects should also contain the originally received json data in the OriginalDaa property - - The last used id, use NextId() to get the next id and up this @@ -254,9 +234,9 @@ A default serializer - + - Last id used + Provided client options @@ -280,24 +260,22 @@ The data to parse - + Deserialize a string into an object The type to deserialize into The data to deserialize - Whether or not the parsing should be checked for missing properties (will output data to the logging if log verbosity is Debug) A specific serializer to use Id of the request the data is returned from (used for grouping logging by request) - + Deserialize a JToken into an object The type to deserialize into The data to deserialize - Whether or not the parsing should be checked for missing properties (will output data to the logging if log verbosity is Debug) A specific serializer to use Id of the request the data is returned from (used for grouping logging by request) @@ -1233,31 +1211,11 @@ The factory for creating requests. Used for unit testing - - - What should happen when hitting a rate limit - - - - - List of active rate limiters - - The total amount of requests made - - - The base address of the API - - - - - Client name - - Adds a rate limiter to the client. There are 2 choices, the and the . @@ -1269,69 +1227,24 @@ Removes all rate limiters from this client - + - Ping to see if the server is reachable + Client options - The roundtrip time of the ping request - - - - Ping to see if the server is reachable - - The roundtrip time of the ping request Base class for socket API implementations - + - The factory for creating sockets. Used for unit testing + Client options - - - The time in between reconnect attempts - - - - - Whether the client should try to auto reconnect when losing connection - - - - - The base address of the API - - - - - - - - - - - The max amount of concurrent socket connections - - - - - - - - - - - - - - - The current kilobytes per second of data being received by all connection from this client, averaged over the last 3 seconds + Incoming kilobytes per second of data @@ -2273,6 +2186,14 @@ If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property + + + Copy the values of the def to the input + + + + + @@ -2281,39 +2202,11 @@ Base for order book options - - - The name of the order book implementation - - Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages. - - - Whether each update should have a consecutive id number. Used to identify and reconnect when numbers are skipped. - - - - - Whether or not a level should be removed from the book when it's pushed out of scope of the limit. For example with a book of limit 10, - when a new bid level is added which makes the total amount of bids 11, should the last bid entry be removed - - - - - ctor - - The name of the order book implementation - Whether each update should have a consecutive id number. Used to identify and reconnect when numbers are skipped. - Whether or not a level should be removed from the book when it's pushed out of scope of the limit. For example with a book of limit 10, - when a new bid is added which makes the total amount of bids 11, should the last bid entry be removed - - - - Base client options @@ -2329,21 +2222,18 @@ The api credentials - - - Should check objects for missing properties based on the model and the received JSON - - Proxy to use - + - ctor + Copy the values of the def to the input - The base address to use + + + @@ -2373,25 +2263,13 @@ Http client to use. If a HttpClient is provided in this property the RequestTimeout and Proxy options will be ignored in requests and should be set on the provided HttpClient instance - + - ctor - - The base address of the API - - - - ctor - - The base address of the API - Shared http client instance - - - - Create a copy of the options + Copy the values of the def to the input - + + @@ -2443,18 +2321,13 @@ single connection will also increase the amount of traffic on that single connection, potentially leading to issues. - + - ctor - - The base address to use - - - - Create a copy of the options + Copy the values of the def to the input - + + @@ -2514,6 +2387,16 @@ The log + + + Whether update numbers are consecutive + + + + + Whether levels should be strictly enforced + + If order book is set @@ -2599,10 +2482,11 @@ BestBid/BesAsk returned as a pair - + ctor + @@ -2936,16 +2820,6 @@ What request body should be set when no data is send (only used in combination with postParametersPosition.InBody) - - - Timeout for requests. This setting is ignored when injecting a HttpClient in the options, requests timeouts should be set on the client then. - - - - - What should happen when running into a rate limit - - List of rate limiters @@ -2961,6 +2835,11 @@ Request headers to be sent with each request + + + Client options + + ctor @@ -2980,18 +2859,6 @@ Removes all rate limiters from this client - - - Ping to see if the server is reachable - - The roundtrip time of the ping request - - - - Ping to see if the server is reachable - - The roundtrip time of the ping request - Execute a request to the uri and deserialize the response into the provided type parameter @@ -3077,35 +2944,11 @@ Semaphore used while creating sockets - - - - - - - - - - - - The max amount of concurrent socket connections - - - - - - - - - - - - Delegate used for processing byte data received from socket connections before it is processed by handlers @@ -3157,6 +3000,11 @@ The current kilobytes per second of data being received by all connection from this client, averaged over the last 3 seconds + + + Client options + + ctor @@ -3312,7 +3160,7 @@ - + Add a subscription to a connection @@ -3943,7 +3791,6 @@ - @@ -3954,7 +3801,6 @@ - @@ -4053,5 +3899,148 @@ + + + Specifies that is allowed as an input even if the + corresponding type disallows it. + + + + + Initializes a new instance of the class. + + + + + Specifies that is disallowed as an input even if the + corresponding type allows it. + + + + + Initializes a new instance of the class. + + + + + Specifies that a method that will never return under any circumstance. + + + + + Initializes a new instance of the class. + + + + + Specifies that the method will not return if the associated + parameter is passed the specified value. + + + + + Gets the condition parameter value. + Code after the method is considered unreachable by diagnostics if the argument + to the associated parameter matches this value. + + + + + Initializes a new instance of the + class with the specified parameter value. + + + The condition parameter value. + Code after the method is considered unreachable by diagnostics if the argument + to the associated parameter matches this value. + + + + + Specifies that an output may be even if the + corresponding type disallows it. + + + + + Initializes a new instance of the class. + + + + + Specifies that when a method returns , + the parameter may be even if the corresponding type disallows it. + + + + + Gets the return value condition. + If the method returns this value, the associated parameter may be . + + + + + Initializes the attribute with the specified return value condition. + + + The return value condition. + If the method returns this value, the associated parameter may be . + + + + + Specifies that an output is not even if the + corresponding type allows it. + + + + + Initializes a new instance of the class. + + + + + Specifies that the output will be non- if the + named parameter is non-. + + + + + Gets the associated parameter name. + The output will be non- if the argument to the + parameter specified is non-. + + + + + Initializes the attribute with the associated parameter name. + + + The associated parameter name. + The output will be non- if the argument to the + parameter specified is non-. + + + + + Specifies that when a method returns , + the parameter will not be even if the corresponding type allows it. + + + + + Gets the return value condition. + If the method returns this value, the associated parameter will not be . + + + + + Initializes the attribute with the specified return value condition. + + + The return value condition. + If the method returns this value, the associated parameter will not be . + + diff --git a/CryptoExchange.Net/Interfaces/IRestClient.cs b/CryptoExchange.Net/Interfaces/IRestClient.cs index 65c8c30..3778958 100644 --- a/CryptoExchange.Net/Interfaces/IRestClient.cs +++ b/CryptoExchange.Net/Interfaces/IRestClient.cs @@ -17,31 +17,11 @@ namespace CryptoExchange.Net.Interfaces /// IRequestFactory RequestFactory { get; set; } - /// - /// What should happen when hitting a rate limit - /// - RateLimitingBehaviour RateLimitBehaviour { get; } - - /// - /// List of active rate limiters - /// - IEnumerable RateLimiters { get; } - /// /// The total amount of requests made /// int TotalRequestsMade { get; } - /// - /// The base address of the API - /// - string BaseAddress { get; } - - /// - /// Client name - /// - string ExchangeName { get; } - /// /// Adds a rate limiter to the client. There are 2 choices, the and the . /// @@ -54,15 +34,8 @@ namespace CryptoExchange.Net.Interfaces void RemoveRateLimiters(); /// - /// Ping to see if the server is reachable + /// Client options /// - /// The roundtrip time of the ping request - CallResult Ping(CancellationToken ct = default); - - /// - /// Ping to see if the server is reachable - /// - /// The roundtrip time of the ping request - Task> PingAsync(CancellationToken ct = default); + RestClientOptions ClientOptions { get; } } } \ No newline at end of file diff --git a/CryptoExchange.Net/Interfaces/ISocketClient.cs b/CryptoExchange.Net/Interfaces/ISocketClient.cs index 3b0b5c8..12964de 100644 --- a/CryptoExchange.Net/Interfaces/ISocketClient.cs +++ b/CryptoExchange.Net/Interfaces/ISocketClient.cs @@ -11,48 +11,14 @@ namespace CryptoExchange.Net.Interfaces public interface ISocketClient: IDisposable { /// - /// The factory for creating sockets. Used for unit testing + /// Client options /// - IWebsocketFactory SocketFactory { get; set; } + SocketClientOptions ClientOptions { get; } /// - /// The time in between reconnect attempts + /// Incoming kilobytes per second of data /// - TimeSpan ReconnectInterval { get; } - - /// - /// Whether the client should try to auto reconnect when losing connection - /// - bool AutoReconnect { get; } - - /// - /// The base address of the API - /// - string BaseAddress { get; } - - /// - TimeSpan ResponseTimeout { get; } - - /// - TimeSpan SocketNoDataTimeout { get; } - - /// - /// The max amount of concurrent socket connections - /// - int MaxSocketConnections { get; } - - /// - int SocketCombineTarget { get; } - /// - int? MaxReconnectTries { get; } - /// - int? MaxResubscribeTries { get; } - /// - int MaxConcurrentResubscriptionsPerSocket { get; } - /// - /// The current kilobytes per second of data being received by all connection from this client, averaged over the last 3 seconds - /// - double IncomingKbps { get; } + public double IncomingKbps { get; } /// /// Unsubscribe from a stream diff --git a/CryptoExchange.Net/Objects/Options.cs b/CryptoExchange.Net/Objects/Options.cs index e452a05..45c22f4 100644 --- a/CryptoExchange.Net/Objects/Options.cs +++ b/CryptoExchange.Net/Objects/Options.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net.Http; using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Interfaces; @@ -16,7 +17,7 @@ namespace CryptoExchange.Net.Objects /// /// The minimum log level to output. Setting it to null will send all messages to the registered ILoggers. /// - public LogLevel? LogLevel { get; set; } = Microsoft.Extensions.Logging.LogLevel.Information; + public LogLevel LogLevel { get; set; } = LogLevel.Information; /// /// The log writers @@ -28,6 +29,19 @@ namespace CryptoExchange.Net.Objects /// public bool OutputOriginalData { get; set; } = false; + /// + /// Copy the values of the def to the input + /// + /// + /// + /// + public void Copy(T input, T def) where T : BaseOptions + { + input.LogLevel = def.LogLevel; + input.LogWriters = def.LogWriters.ToList(); + input.OutputOriginalData = def.OutputOriginalData; + } + /// public override string ToString() { @@ -39,47 +53,11 @@ namespace CryptoExchange.Net.Objects /// Base for order book options /// public class OrderBookOptions : BaseOptions - { - /// - /// The name of the order book implementation - /// - public string OrderBookName { get; } - + { /// /// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages. /// public bool ChecksumValidationEnabled { get; set; } = true; - - /// - /// Whether each update should have a consecutive id number. Used to identify and reconnect when numbers are skipped. - /// - public bool SequenceNumbersAreConsecutive { get; } - - /// - /// Whether or not a level should be removed from the book when it's pushed out of scope of the limit. For example with a book of limit 10, - /// when a new bid level is added which makes the total amount of bids 11, should the last bid entry be removed - /// - public bool StrictLevels { get; } - - /// - /// ctor - /// - /// The name of the order book implementation - /// Whether each update should have a consecutive id number. Used to identify and reconnect when numbers are skipped. - /// Whether or not a level should be removed from the book when it's pushed out of scope of the limit. For example with a book of limit 10, - /// when a new bid is added which makes the total amount of bids 11, should the last bid entry be removed - public OrderBookOptions(string name, bool sequencesAreConsecutive, bool strictLevels) - { - OrderBookName = name; - SequenceNumbersAreConsecutive = sequencesAreConsecutive; - StrictLevels = strictLevels; - } - - /// - public override string ToString() - { - return $"{base.ToString()}, OrderBookName: {OrderBookName}, SequenceNumbersAreConsequtive: {SequenceNumbersAreConsecutive}, StrictLevels: {StrictLevels}"; - } } /// @@ -87,7 +65,7 @@ namespace CryptoExchange.Net.Objects /// public class ClientOptions : BaseOptions { - private string _baseAddress; + private string _baseAddress = string.Empty; /// /// The base address of the client @@ -97,6 +75,9 @@ namespace CryptoExchange.Net.Objects get => _baseAddress; set { + if (value == null) + return; + var newValue = value; if (!newValue.EndsWith("/")) newValue += "/"; @@ -109,25 +90,24 @@ namespace CryptoExchange.Net.Objects /// public ApiCredentials? ApiCredentials { get; set; } - /// - /// Should check objects for missing properties based on the model and the received JSON - /// - public bool ShouldCheckObjects { get; set; } = false; - /// /// Proxy to use /// public ApiProxy? Proxy { get; set; } /// - /// ctor + /// Copy the values of the def to the input /// - /// The base address to use -#pragma warning disable 8618 - public ClientOptions(string baseAddress) -#pragma warning restore 8618 + /// + /// + /// + public new void Copy(T input, T def) where T : ClientOptions { - BaseAddress = baseAddress; + base.Copy(input, def); + + input.BaseAddress = def.BaseAddress; + input.ApiCredentials = def.ApiCredentials?.Copy(); + input.Proxy = def.Proxy; } /// @@ -163,44 +143,19 @@ namespace CryptoExchange.Net.Objects public HttpClient? HttpClient { get; set; } /// - /// ctor - /// - /// The base address of the API - public RestClientOptions(string baseAddress): base(baseAddress) - { - } - /// - /// ctor - /// - /// The base address of the API - /// Shared http client instance - public RestClientOptions(HttpClient httpClient, string baseAddress) : base(baseAddress) - { - HttpClient = httpClient; - } - /// - /// Create a copy of the options + /// Copy the values of the def to the input /// /// - /// - public T Copy() where T : RestClientOptions, new() + /// + /// + public new void Copy(T input, T def) where T : RestClientOptions { - var copy = new T - { - BaseAddress = BaseAddress, - LogLevel = LogLevel, - Proxy = Proxy, - LogWriters = LogWriters, - RateLimiters = RateLimiters, - RateLimitingBehaviour = RateLimitingBehaviour, - RequestTimeout = RequestTimeout, - HttpClient = HttpClient - }; - - if (ApiCredentials != null) - copy.ApiCredentials = ApiCredentials.Copy(); - - return copy; + base.Copy(input, def); + + input.HttpClient = def.HttpClient; + input.RateLimiters = def.RateLimiters.ToList(); + input.RateLimitingBehaviour = def.RateLimitingBehaviour; + input.RequestTimeout = def.RequestTimeout; } /// @@ -257,36 +212,23 @@ namespace CryptoExchange.Net.Objects public int? SocketSubscriptionsCombineTarget { get; set; } /// - /// ctor - /// - /// The base address to use - public SocketClientOptions(string baseAddress) : base(baseAddress) - { - } - - /// - /// Create a copy of the options + /// Copy the values of the def to the input /// /// - /// - public T Copy() where T : SocketClientOptions, new() + /// + /// + public new void Copy(T input, T def) where T : SocketClientOptions { - var copy = new T - { - BaseAddress = BaseAddress, - LogLevel = LogLevel, - Proxy = Proxy, - LogWriters = LogWriters, - AutoReconnect = AutoReconnect, - ReconnectInterval = ReconnectInterval, - SocketResponseTimeout = SocketResponseTimeout, - SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget - }; + base.Copy(input, def); - if (ApiCredentials != null) - copy.ApiCredentials = ApiCredentials.Copy(); - - return copy; + input.AutoReconnect = def.AutoReconnect; + input.ReconnectInterval = def.ReconnectInterval; + input.MaxReconnectTries = def.MaxReconnectTries; + input.MaxResubscribeTries = def.MaxResubscribeTries; + input.MaxConcurrentResubscriptionsPerSocket = def.MaxConcurrentResubscriptionsPerSocket; + input.SocketResponseTimeout = def.SocketResponseTimeout; + input.SocketNoDataTimeout = def.SocketNoDataTimeout; + input.SocketSubscriptionsCombineTarget = def.SocketSubscriptionsCombineTarget; } /// diff --git a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs index 2a8c937..17d9136 100644 --- a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs +++ b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs @@ -35,8 +35,7 @@ namespace CryptoExchange.Net.OrderBook private OrderBookStatus status; private UpdateSubscription? subscription; - private readonly bool sequencesAreConsecutive; - private readonly bool strictLevels; + private readonly bool validateChecksum; private bool _stopProcessing; @@ -53,6 +52,16 @@ namespace CryptoExchange.Net.OrderBook /// protected Log log; + /// + /// Whether update numbers are consecutive + /// + protected bool sequencesAreConsecutive; + + /// + /// Whether levels should be strictly enforced + /// + protected bool strictLevels; + /// /// If order book is set /// @@ -199,9 +208,10 @@ namespace CryptoExchange.Net.OrderBook /// /// ctor /// + /// /// /// - protected SymbolOrderBook(string symbol, OrderBookOptions options) + protected SymbolOrderBook(string id, string symbol, OrderBookOptions options) { if (symbol == null) throw new ArgumentNullException(nameof(symbol)); @@ -209,13 +219,11 @@ namespace CryptoExchange.Net.OrderBook if (options == null) throw new ArgumentNullException(nameof(options)); - Id = options.OrderBookName; + Id = id; processBuffer = new List(); _processQueue = new ConcurrentQueue(); _queueEvent = new AutoResetEvent(false); - sequencesAreConsecutive = options.SequenceNumbersAreConsecutive; - strictLevels = options.StrictLevels; validateChecksum = options.ChecksumValidationEnabled; Symbol = symbol; Status = OrderBookStatus.Disconnected; @@ -223,7 +231,7 @@ namespace CryptoExchange.Net.OrderBook asks = new SortedList(); bids = new SortedList(new DescComparer()); - log = new Log(options.OrderBookName) { Level = options.LogLevel }; + log = new Log(id) { Level = options.LogLevel }; var writers = options.LogWriters ?? new List { new DebugLogger() }; log.UpdateWriters(writers.ToList()); } diff --git a/CryptoExchange.Net/RestClient.cs b/CryptoExchange.Net/RestClient.cs index 54fd412..2ded49b 100644 --- a/CryptoExchange.Net/RestClient.cs +++ b/CryptoExchange.Net/RestClient.cs @@ -61,19 +61,12 @@ namespace CryptoExchange.Net /// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody) /// protected string requestBodyEmptyContent = "{}"; - - /// - /// Timeout for requests. This setting is ignored when injecting a HttpClient in the options, requests timeouts should be set on the client then. - /// - public TimeSpan RequestTimeout { get; } - /// - /// What should happen when running into a rate limit - /// - public RateLimitingBehaviour RateLimitBehaviour { get; } + /// /// List of rate limiters /// - public IEnumerable RateLimiters { get; private set; } + protected IEnumerable RateLimiters { get; private set; } + /// /// Total requests made by this client /// @@ -84,6 +77,11 @@ namespace CryptoExchange.Net /// protected Dictionary? StandardRequestHeaders { get; set; } + /// + /// Client options + /// + public new RestClientOptions ClientOptions { get; } + /// /// ctor /// @@ -95,9 +93,9 @@ namespace CryptoExchange.Net if (exchangeOptions == null) throw new ArgumentNullException(nameof(exchangeOptions)); - RequestTimeout = exchangeOptions.RequestTimeout; + ClientOptions = exchangeOptions; RequestFactory.Configure(exchangeOptions.RequestTimeout, exchangeOptions.Proxy, exchangeOptions.HttpClient); - RateLimitBehaviour = exchangeOptions.RateLimitingBehaviour; + var rateLimiters = new List(); foreach (var rateLimiter in exchangeOptions.RateLimiters) rateLimiters.Add(rateLimiter); @@ -126,48 +124,6 @@ namespace CryptoExchange.Net RateLimiters = new List(); } - /// - /// Ping to see if the server is reachable - /// - /// The roundtrip time of the ping request - public virtual CallResult Ping(CancellationToken ct = default) => PingAsync(ct).Result; - - /// - /// Ping to see if the server is reachable - /// - /// The roundtrip time of the ping request - public virtual async Task> PingAsync(CancellationToken ct = default) - { - var ping = new Ping(); - var uri = new Uri(BaseAddress); - PingReply reply; - - var ctRegistration = ct.Register(() => ping.SendAsyncCancel()); - try - { - reply = await ping.SendPingAsync(uri.Host).ConfigureAwait(false); - } - catch (PingException e) - { - if (e.InnerException == null) - return new CallResult(0, new CantConnectError { Message = "Ping failed: " + e.Message }); - - if (e.InnerException is SocketException exception) - return new CallResult(0, new CantConnectError { Message = "Ping failed: " + exception.SocketErrorCode }); - return new CallResult(0, new CantConnectError { Message = "Ping failed: " + e.InnerException.Message }); - } - finally - { - ctRegistration.Dispose(); - ping.Dispose(); - } - - if (ct.IsCancellationRequested) - return new CallResult(0, new CancellationRequestedError()); - - return reply.Status == IPStatus.Success ? new CallResult(reply.RoundtripTime, null) : new CallResult(0, new CantConnectError { Message = "Ping failed: " + reply.Status }); - } - /// /// Execute a request to the uri and deserialize the response into the provided type parameter /// @@ -210,7 +166,7 @@ namespace CryptoExchange.Net var request = ConstructRequest(uri, method, parameters, signed, paramsPosition, arraySerialization ?? this.arraySerialization, requestId, additionalHeaders); foreach (var limiter in RateLimiters) { - var limitResult = limiter.LimitRequest(this, uri.AbsolutePath, RateLimitBehaviour, credits); + var limitResult = limiter.LimitRequest(this, uri.AbsolutePath, ClientOptions.RateLimitingBehaviour, credits); if (!limitResult.Success) { log.Write(LogLevel.Information, $"[{requestId}] Request {uri.AbsolutePath} failed because of rate limit"); @@ -232,7 +188,7 @@ namespace CryptoExchange.Net paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")); } - log.Write(LogLevel.Debug, $"[{requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(apiProxy == null ? "" : $" via proxy {apiProxy.Host}")}"); + log.Write(LogLevel.Debug, $"[{requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(ClientOptions.Proxy == null ? "" : $" via proxy {ClientOptions.Proxy.Host}")}"); return await GetResponseAsync(request, deserializer, cancellationToken).ConfigureAwait(false); } @@ -277,8 +233,8 @@ namespace CryptoExchange.Net return WebCallResult.CreateErrorResult(response.StatusCode, response.ResponseHeaders, error); // Not an error, so continue deserializing - var deserializeResult = Deserialize(parseResult.Data, null, deserializer, request.RequestId); - return new WebCallResult(response.StatusCode, response.ResponseHeaders, OutputOriginalData ? data: null, deserializeResult.Data, deserializeResult.Error); + var deserializeResult = Deserialize(parseResult.Data, deserializer, request.RequestId); + return new WebCallResult(response.StatusCode, response.ResponseHeaders, ClientOptions.OutputOriginalData ? data: null, deserializeResult.Data, deserializeResult.Error); } else { @@ -287,7 +243,7 @@ namespace CryptoExchange.Net responseStream.Close(); response.Close(); - return new WebCallResult(statusCode, headers, OutputOriginalData ? desResult.OriginalData : null, desResult.Data, desResult.Error); + return new WebCallResult(statusCode, headers, ClientOptions.OutputOriginalData ? desResult.OriginalData : null, desResult.Data, desResult.Error); } } else diff --git a/CryptoExchange.Net/SocketClient.cs b/CryptoExchange.Net/SocketClient.cs index e039911..714e385 100644 --- a/CryptoExchange.Net/SocketClient.cs +++ b/CryptoExchange.Net/SocketClient.cs @@ -35,27 +35,10 @@ namespace CryptoExchange.Net /// Semaphore used while creating sockets /// protected internal readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1); - - /// - public TimeSpan ReconnectInterval { get; } - /// - public bool AutoReconnect { get; } - /// - public TimeSpan ResponseTimeout { get; } - /// - public TimeSpan SocketNoDataTimeout { get; } /// /// The max amount of concurrent socket connections /// - public int MaxSocketConnections { get; protected set; } = 9999; - /// - public int SocketCombineTarget { get; protected set; } - /// - public int? MaxReconnectTries { get; protected set; } - /// - public int? MaxResubscribeTries { get; protected set; } - /// - public int MaxConcurrentResubscriptionsPerSocket { get; protected set; } + protected int MaxSocketConnections { get; set; } = 9999; /// /// Delegate used for processing byte data received from socket connections before it is processed by handlers /// @@ -110,6 +93,12 @@ namespace CryptoExchange.Net return sockets.Sum(s => s.Value.Socket.IncomingKbps); } } + + /// + /// Client options + /// + public new SocketClientOptions ClientOptions { get; } + #endregion /// @@ -123,14 +112,7 @@ namespace CryptoExchange.Net if (exchangeOptions == null) throw new ArgumentNullException(nameof(exchangeOptions)); - AutoReconnect = exchangeOptions.AutoReconnect; - ReconnectInterval = exchangeOptions.ReconnectInterval; - ResponseTimeout = exchangeOptions.SocketResponseTimeout; - SocketNoDataTimeout = exchangeOptions.SocketNoDataTimeout; - SocketCombineTarget = exchangeOptions.SocketSubscriptionsCombineTarget ?? 1; - MaxReconnectTries = exchangeOptions.MaxReconnectTries; - MaxResubscribeTries = exchangeOptions.MaxResubscribeTries; - MaxConcurrentResubscriptionsPerSocket = exchangeOptions.MaxConcurrentResubscriptionsPerSocket; + ClientOptions = exchangeOptions; } /// @@ -156,7 +138,7 @@ namespace CryptoExchange.Net /// protected virtual Task> SubscribeAsync(object? request, string? identifier, bool authenticated, Action> dataHandler, CancellationToken ct) { - return SubscribeAsync(BaseAddress, request, identifier, authenticated, dataHandler, ct); + return SubscribeAsync(ClientOptions.BaseAddress, request, identifier, authenticated, dataHandler, ct); } /// @@ -184,8 +166,8 @@ namespace CryptoExchange.Net socketConnection = GetSocketConnection(url, authenticated); // Add a subscription on the socket connection - subscription = AddSubscription(request, identifier, true, socketConnection, dataHandler, ct); - if (SocketCombineTarget == 1) + subscription = AddSubscription(request, identifier, true, socketConnection, dataHandler); + if (ClientOptions.SocketSubscriptionsCombineTarget == 1) { // Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway semaphoreSlim.Release(); @@ -251,7 +233,7 @@ namespace CryptoExchange.Net protected internal virtual async Task> SubscribeAndWaitAsync(SocketConnection socketConnection, object request, SocketSubscription subscription) { CallResult? callResult = null; - await socketConnection.SendAndWaitAsync(request, ResponseTimeout, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false); + await socketConnection.SendAndWaitAsync(request, ClientOptions.SocketResponseTimeout, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false); if (callResult?.Success == true) subscription.Confirmed = true; @@ -268,7 +250,7 @@ namespace CryptoExchange.Net /// protected virtual Task> QueryAsync(object request, bool authenticated) { - return QueryAsync(BaseAddress, request, authenticated); + return QueryAsync(ClientOptions.BaseAddress, request, authenticated); } /// @@ -287,7 +269,7 @@ namespace CryptoExchange.Net try { socketConnection = GetSocketConnection(url, authenticated); - if (SocketCombineTarget == 1) + if (ClientOptions.SocketSubscriptionsCombineTarget == 1) { // Can release early when only a single sub per connection semaphoreSlim.Release(); @@ -325,7 +307,7 @@ namespace CryptoExchange.Net protected virtual async Task> QueryAndWaitAsync(SocketConnection socket, object request) { var dataResult = new CallResult(default, new ServerError("No response on query received")); - await socket.SendAndWaitAsync(request, ResponseTimeout, data => + await socket.SendAndWaitAsync(request, ClientOptions.SocketResponseTimeout, data => { if (!HandleQueryResponse(socket, request, data, out var callResult)) return false; @@ -445,25 +427,25 @@ namespace CryptoExchange.Net /// The socket connection the handler is on /// The handler of the data received /// - protected virtual SocketSubscription AddSubscription(object? request, string? identifier, bool userSubscription, SocketConnection connection, Action> dataHandler, CancellationToken ct) + protected virtual SocketSubscription AddSubscription(object? request, string? identifier, bool userSubscription, SocketConnection connection, Action> dataHandler) { void InternalHandler(MessageEvent messageEvent) { if (typeof(T) == typeof(string)) { var stringData = (T)Convert.ChangeType(messageEvent.JsonData.ToString(), typeof(T)); - dataHandler(new DataEvent(stringData, null, OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp)); + dataHandler(new DataEvent(stringData, null, ClientOptions.OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp)); return; } - var desResult = Deserialize(messageEvent.JsonData, false); + var desResult = Deserialize(messageEvent.JsonData); if (!desResult) { log.Write(LogLevel.Warning, $"Socket {connection.Socket.Id} Failed to deserialize data into type {typeof(T)}: {desResult.Error}"); return; } - dataHandler(new DataEvent(desResult.Data, null, OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp)); + dataHandler(new DataEvent(desResult.Data, null, ClientOptions.OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp)); } var subscription = request == null @@ -499,7 +481,7 @@ namespace CryptoExchange.Net var result = socketResult.Equals(default(KeyValuePair)) ? null : socketResult.Value; if (result != null) { - if (result.SubscriptionCount < SocketCombineTarget || (sockets.Count >= MaxSocketConnections && sockets.All(s => s.Value.SubscriptionCount >= SocketCombineTarget))) + if (result.SubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (sockets.Count >= MaxSocketConnections && sockets.All(s => s.Value.SubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget))) { // Use existing socket if it has less than target connections OR it has the least connections and we can't make new return result; @@ -554,10 +536,10 @@ namespace CryptoExchange.Net var socket = SocketFactory.CreateWebsocket(log, address); log.Write(LogLevel.Debug, $"Socket {socket.Id} new socket created for " + address); - if (apiProxy != null) - socket.SetProxy(apiProxy); + if (ClientOptions.Proxy != null) + socket.SetProxy(ClientOptions.Proxy); - socket.Timeout = SocketNoDataTimeout; + socket.Timeout = ClientOptions.SocketNoDataTimeout; socket.DataInterpreterBytes = dataInterpreterBytes; socket.DataInterpreterString = dataInterpreterString; socket.RatelimitPerSecond = RateLimitPerSocketPerSecond; diff --git a/CryptoExchange.Net/Sockets/SocketConnection.cs b/CryptoExchange.Net/Sockets/SocketConnection.cs index 78bc1db..1df5dc9 100644 --- a/CryptoExchange.Net/Sockets/SocketConnection.cs +++ b/CryptoExchange.Net/Sockets/SocketConnection.cs @@ -129,7 +129,7 @@ namespace CryptoExchange.Net.Sockets subscriptions = new List(); Socket = socket; - Socket.Timeout = client.SocketNoDataTimeout; + Socket.Timeout = client.ClientOptions.SocketNoDataTimeout; Socket.OnMessage += ProcessMessage; Socket.OnClose += SocketOnClose; Socket.OnOpen += SocketOnOpen; @@ -183,7 +183,7 @@ namespace CryptoExchange.Net.Sockets } // Message was not a request response, check data handlers - var messageEvent = new MessageEvent(this, tokenData, socketClient.OutputOriginalData ? data: null, timestamp); + var messageEvent = new MessageEvent(this, tokenData, socketClient.ClientOptions.OutputOriginalData ? data: null, timestamp); if (!HandleData(messageEvent) && !handledResponse) { if (!socketClient.UnhandledMessageExpected) @@ -330,7 +330,7 @@ namespace CryptoExchange.Net.Sockets } } - if (socketClient.AutoReconnect && ShouldReconnect) + if (socketClient.ClientOptions.AutoReconnect && ShouldReconnect) { if (Socket.Reconnecting) return; // Already reconnecting @@ -338,7 +338,7 @@ namespace CryptoExchange.Net.Sockets Socket.Reconnecting = true; DisconnectTime = DateTime.UtcNow; - log.Write(LogLevel.Information, $"Socket {Socket.Id} Connection lost, will try to reconnect after {socketClient.ReconnectInterval}"); + log.Write(LogLevel.Information, $"Socket {Socket.Id} Connection lost, will try to reconnect after {socketClient.ClientOptions.ReconnectInterval}"); if (!lostTriggered) { lostTriggered = true; @@ -350,7 +350,7 @@ namespace CryptoExchange.Net.Sockets while (ShouldReconnect) { // Wait a bit before attempting reconnect - await Task.Delay(socketClient.ReconnectInterval).ConfigureAwait(false); + await Task.Delay(socketClient.ClientOptions.ReconnectInterval).ConfigureAwait(false); if (!ShouldReconnect) { // Should reconnect changed to false while waiting to reconnect @@ -363,8 +363,8 @@ namespace CryptoExchange.Net.Sockets { ReconnectTry++; ResubscribeTry = 0; - if (socketClient.MaxReconnectTries != null - && ReconnectTry >= socketClient.MaxReconnectTries) + if (socketClient.ClientOptions.MaxReconnectTries != null + && ReconnectTry >= socketClient.ClientOptions.MaxReconnectTries) { log.Write(LogLevel.Debug, $"Socket {Socket.Id} failed to reconnect after {ReconnectTry} tries, closing"); ShouldReconnect = false; @@ -377,7 +377,7 @@ namespace CryptoExchange.Net.Sockets break; } - log.Write(LogLevel.Debug, $"Socket {Socket.Id} failed to reconnect{(socketClient.MaxReconnectTries != null ? $", try {ReconnectTry}/{socketClient.MaxReconnectTries}": "")}"); + log.Write(LogLevel.Debug, $"Socket {Socket.Id} failed to reconnect{(socketClient.ClientOptions.MaxReconnectTries != null ? $", try {ReconnectTry}/{socketClient.ClientOptions.MaxReconnectTries}": "")}"); continue; } @@ -392,8 +392,8 @@ namespace CryptoExchange.Net.Sockets { ResubscribeTry++; - if (socketClient.MaxResubscribeTries != null && - ResubscribeTry >= socketClient.MaxResubscribeTries) + if (socketClient.ClientOptions.MaxResubscribeTries != null && + ResubscribeTry >= socketClient.ClientOptions.MaxResubscribeTries) { log.Write(LogLevel.Debug, $"Socket {Socket.Id} failed to resubscribe after {ResubscribeTry} tries, closing"); ShouldReconnect = false; @@ -405,7 +405,7 @@ namespace CryptoExchange.Net.Sockets _ = Task.Run(() => ConnectionClosed?.Invoke()); } else - log.Write(LogLevel.Debug, $"Socket {Socket.Id} resubscribing all subscriptions failed on reconnected socket{(socketClient.MaxResubscribeTries != null ? $", try {ResubscribeTry}/{socketClient.MaxResubscribeTries}" : "")}. Disconnecting and reconnecting."); + log.Write(LogLevel.Debug, $"Socket {Socket.Id} resubscribing all subscriptions failed on reconnected socket{(socketClient.ClientOptions.MaxResubscribeTries != null ? $", try {ResubscribeTry}/{socketClient.ClientOptions.MaxResubscribeTries}" : "")}. Disconnecting and reconnecting."); if (Socket.IsOpen) await Socket.CloseAsync().ConfigureAwait(false); @@ -431,7 +431,7 @@ namespace CryptoExchange.Net.Sockets } else { - if (!socketClient.AutoReconnect && ShouldReconnect) + if (!socketClient.ClientOptions.AutoReconnect && ShouldReconnect) _ = Task.Run(() => ConnectionClosed?.Invoke()); // No reconnecting needed @@ -472,11 +472,11 @@ namespace CryptoExchange.Net.Sockets subscriptionList = subscriptions.Where(h => h.Request != null).ToList(); // Foreach subscription which is subscribed by a subscription request we will need to resend that request to resubscribe - for (var i = 0; i < subscriptionList.Count; i += socketClient.MaxConcurrentResubscriptionsPerSocket) + for (var i = 0; i < subscriptionList.Count; i += socketClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket) { var success = true; var taskList = new List(); - foreach (var subscription in subscriptionList.Skip(i).Take(socketClient.MaxConcurrentResubscriptionsPerSocket)) + foreach (var subscription in subscriptionList.Skip(i).Take(socketClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket)) { if (!Socket.IsOpen) continue; diff --git a/CryptoExchange.Net/Sockets/SocketSubscription.cs b/CryptoExchange.Net/Sockets/SocketSubscription.cs index c1a063f..77058ce 100644 --- a/CryptoExchange.Net/Sockets/SocketSubscription.cs +++ b/CryptoExchange.Net/Sockets/SocketSubscription.cs @@ -61,7 +61,6 @@ namespace CryptoExchange.Net.Sockets /// /// /// - /// /// public static SocketSubscription CreateForRequest(int id, object request, bool userSubscription, Action dataHandler) @@ -76,7 +75,6 @@ namespace CryptoExchange.Net.Sockets /// /// /// - /// /// public static SocketSubscription CreateForIdentifier(int id, string identifier, bool userSubscription, Action dataHandler)