diff --git a/CryptoExchange.Net/Authentication/ApiCredentials.cs b/CryptoExchange.Net/Authentication/ApiCredentials.cs
index e6f1dcb..f863fcd 100644
--- a/CryptoExchange.Net/Authentication/ApiCredentials.cs
+++ b/CryptoExchange.Net/Authentication/ApiCredentials.cs
@@ -14,17 +14,17 @@ namespace CryptoExchange.Net.Authentication
///
/// The api key to authenticate requests
///
- public SecureString Key { get; }
+ public SecureString? Key { get; }
///
/// The api secret to authenticate requests
///
- public SecureString Secret { get; }
+ public SecureString? Secret { get; }
///
/// The private key to authenticate requests
///
- public PrivateKey PrivateKey { get; }
+ public PrivateKey? PrivateKey { get; }
///
/// Create Api credentials providing a private key for authentication
@@ -56,8 +56,20 @@ namespace CryptoExchange.Net.Authentication
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
throw new ArgumentException("Key and secret can't be null/empty");
- Key = CreateSecureString(key);
- Secret = CreateSecureString(secret);
+ Key = key.ToSecureString();
+ Secret = secret.ToSecureString();
+ }
+
+ ///
+ /// Copy the credentials
+ ///
+ ///
+ public ApiCredentials Copy()
+ {
+ if (PrivateKey == null)
+ return new ApiCredentials(Key!.GetString(), Secret!.GetString());
+ else
+ return new ApiCredentials(PrivateKey!.Copy());
}
///
@@ -66,24 +78,23 @@ namespace CryptoExchange.Net.Authentication
/// The stream containing the json data
/// A key to identify the credentials for the API. For example, when set to `binanceKey` the json data should contain a value for the property `binanceKey`. Defaults to 'apiKey'.
/// A key to identify the credentials for the API. For example, when set to `binanceSecret` the json data should contain a value for the property `binanceSecret`. Defaults to 'apiSecret'.
- public ApiCredentials(Stream inputStream, string identifierKey = null, string identifierSecret = null)
+ public ApiCredentials(Stream inputStream, string? identifierKey = null, string? identifierSecret = null)
{
- using (var reader = new StreamReader(inputStream, Encoding.ASCII, false, 512, true))
- {
- var stringData = reader.ReadToEnd();
- var jsonData = stringData.ToJToken();
- if(jsonData == null)
- throw new ArgumentException("Input stream not valid json data");
+ using var reader = new StreamReader(inputStream, Encoding.ASCII, false, 512, true);
+
+ var stringData = reader.ReadToEnd();
+ var jsonData = stringData.ToJToken();
+ if(jsonData == null)
+ throw new ArgumentException("Input stream not valid json data");
- var key = TryGetValue(jsonData, identifierKey ?? "apiKey");
- var secret = TryGetValue(jsonData, identifierSecret ?? "apiSecret");
+ var key = TryGetValue(jsonData, identifierKey ?? "apiKey");
+ var secret = TryGetValue(jsonData, identifierSecret ?? "apiSecret");
- if (key == null || secret == null)
- throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");
+ if (key == null || secret == null)
+ throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");
- Key = CreateSecureString(key);
- Secret = CreateSecureString(secret);
- }
+ Key = key.ToSecureString();
+ Secret = secret.ToSecureString();
inputStream.Seek(0, SeekOrigin.Begin);
}
@@ -94,26 +105,12 @@ namespace CryptoExchange.Net.Authentication
///
///
///
- protected string TryGetValue(JToken data, string key)
+ protected string? TryGetValue(JToken data, string key)
{
if (data[key] == null)
return null;
return (string) data[key];
- }
-
- ///
- /// Create a secure string from a string
- ///
- ///
- ///
- protected SecureString CreateSecureString(string source)
- {
- var secureString = new SecureString();
- foreach (var c in source)
- secureString.AppendChar(c);
- secureString.MakeReadOnly();
- return secureString;
- }
+ }
///
/// Dispose
diff --git a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs
index d2a41a0..8739fed 100644
--- a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs
+++ b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Net.Http;
namespace CryptoExchange.Net.Authentication
{
@@ -29,7 +30,7 @@ namespace CryptoExchange.Net.Authentication
///
///
///
- public virtual Dictionary AddAuthenticationToParameters(string uri, string method, Dictionary parameters, bool signed)
+ public virtual Dictionary AddAuthenticationToParameters(string uri, HttpMethod method, Dictionary parameters, bool signed)
{
return parameters;
}
@@ -42,7 +43,7 @@ namespace CryptoExchange.Net.Authentication
///
///
///
- public virtual Dictionary AddAuthenticationToHeaders(string uri, string method, Dictionary parameters, bool signed)
+ public virtual Dictionary AddAuthenticationToHeaders(string uri, HttpMethod method, Dictionary parameters, bool signed)
{
return new Dictionary();
}
diff --git a/CryptoExchange.Net/Authentication/PrivateKey.cs b/CryptoExchange.Net/Authentication/PrivateKey.cs
index 47c4755..238e9b8 100644
--- a/CryptoExchange.Net/Authentication/PrivateKey.cs
+++ b/CryptoExchange.Net/Authentication/PrivateKey.cs
@@ -16,7 +16,7 @@ namespace CryptoExchange.Net.Authentication
///
/// The private key's pass phrase
///
- public SecureString Passphrase { get; }
+ public SecureString? Passphrase { get; }
///
/// Indicates if the private key is encrypted or not
@@ -81,15 +81,23 @@ namespace CryptoExchange.Net.Authentication
if (string.IsNullOrEmpty(key))
throw new ArgumentException("Key can't be null/empty");
- var secureKey = new SecureString();
- foreach (var c in key)
- secureKey.AppendChar(c);
- secureKey.MakeReadOnly();
- Key = secureKey;
+ Key = key.ToSecureString();
IsEncrypted = false;
}
+ ///
+ /// Copy the private key
+ ///
+ ///
+ public PrivateKey Copy()
+ {
+ if (Passphrase == null)
+ return new PrivateKey(Key.GetString());
+ else
+ return new PrivateKey(Key.GetString(), Passphrase.GetString());
+ }
+
///
/// Dispose
///
diff --git a/CryptoExchange.Net/BaseClient.cs b/CryptoExchange.Net/BaseClient.cs
index b4e79ed..699f349 100644
--- a/CryptoExchange.Net/BaseClient.cs
+++ b/CryptoExchange.Net/BaseClient.cs
@@ -7,15 +7,17 @@ using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Globalization;
+using System.IO;
using System.Linq;
using System.Reflection;
+using System.Threading.Tasks;
namespace CryptoExchange.Net
{
///
/// The base for all clients
///
- public abstract class BaseClient: IDisposable
+ public abstract class BaseClient : IDisposable
{
///
/// The address of the client
@@ -28,11 +30,11 @@ namespace CryptoExchange.Net
///
/// The api proxy
///
- protected ApiProxy apiProxy;
+ protected ApiProxy? apiProxy;
///
/// The auth provider
///
- protected internal AuthenticationProvider authProvider;
+ protected internal AuthenticationProvider? authProvider;
///
/// The last used id
@@ -59,26 +61,17 @@ namespace CryptoExchange.Net
///
///
///
- protected BaseClient(ClientOptions options, AuthenticationProvider authenticationProvider)
+ protected BaseClient(ClientOptions options, AuthenticationProvider? authenticationProvider)
{
log = new Log();
authProvider = authenticationProvider;
- Configure(options);
- }
+ log.UpdateWriters(options.LogWriters);
+ log.Level = options.LogVerbosity;
- ///
- /// Configure the client using the provided options
- ///
- /// Options
- protected void Configure(ClientOptions clientOptions)
- {
- log.UpdateWriters(clientOptions.LogWriters);
- log.Level = clientOptions.LogVerbosity;
+ BaseAddress = options.BaseAddress;
+ apiProxy = options.Proxy;
- BaseAddress = clientOptions.BaseAddress;
- apiProxy = clientOptions.Proxy;
- if (apiProxy != null)
- log.Write(LogVerbosity.Info, $"Setting api proxy to {clientOptions.Proxy.Host}:{clientOptions.Proxy.Port}");
+ log.Write(LogVerbosity.Debug, $"Client configuration: {options}");
}
///
@@ -137,10 +130,10 @@ namespace CryptoExchange.Net
/// 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
///
- protected CallResult Deserialize(string data, bool checkObject = true, JsonSerializer serializer = null)
+ protected CallResult Deserialize(string data, bool checkObject = true, JsonSerializer? serializer = null)
{
var tokenResult = ValidateJson(data);
- return !tokenResult.Success ? new CallResult(default, tokenResult.Error) : Deserialize(tokenResult.Data, checkObject, serializer);
+ return !tokenResult ? new CallResult(default, tokenResult.Error) : Deserialize(tokenResult.Data, checkObject, serializer);
}
///
@@ -151,7 +144,7 @@ namespace CryptoExchange.Net
/// 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
///
- protected CallResult Deserialize(JToken obj, bool checkObject = true, JsonSerializer serializer = null)
+ protected CallResult Deserialize(JToken obj, bool checkObject = true, JsonSerializer? serializer = null)
{
if (serializer == null)
serializer = defaultSerializer;
@@ -200,6 +193,53 @@ namespace CryptoExchange.Net
}
}
+ ///
+ /// Deserialize a stream into an object
+ ///
+ /// The type to deserialize into
+ /// The stream to deserialize
+ /// A specific serializer to use
+ ///
+ protected async Task> Deserialize(Stream stream, JsonSerializer? serializer = null)
+ {
+ if (serializer == null)
+ serializer = defaultSerializer;
+
+ try
+ {
+ using var reader = new StreamReader(stream);
+ using var jsonReader = new JsonTextReader(reader);
+ return new CallResult(serializer.Deserialize(jsonReader), null);
+ }
+ catch (JsonReaderException jre)
+ {
+ var data = await ReadStream(stream).ConfigureAwait(false);
+ var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}";
+ log.Write(LogVerbosity.Error, info);
+ return new CallResult(default, new DeserializeError(info));
+ }
+ catch (JsonSerializationException jse)
+ {
+ var data = await ReadStream(stream).ConfigureAwait(false);
+ var info = $"Deserialize JsonSerializationException: {jse.Message}, data: {data}";
+ log.Write(LogVerbosity.Error, info);
+ return new CallResult(default, new DeserializeError(info));
+ }
+ catch (Exception ex)
+ {
+ var data = await ReadStream(stream).ConfigureAwait(false);
+ var info = $"Deserialize Unknown Exception: {ex.Message}, data: {data}";
+ log.Write(LogVerbosity.Error, info);
+ return new CallResult(default, new DeserializeError(info));
+ }
+ }
+
+ private async Task ReadStream(Stream stream)
+ {
+ using var reader = new StreamReader(stream);
+ return await reader.ReadToEndAsync().ConfigureAwait(false);
+ }
+
private void CheckObject(Type type, JObject obj)
{
if (type.GetCustomAttribute(true) != null)
@@ -233,13 +273,17 @@ namespace CryptoExchange.Net
if (d == null)
{
d = properties.SingleOrDefault(p => string.Equals(p, token.Key, StringComparison.CurrentCultureIgnoreCase));
- if (d == null && !(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)))
+ if (d == null)
{
- log.Write(LogVerbosity.Warning, $"Local object doesn't have property `{token.Key}` expected in type `{type.Name}`");
- isDif = true;
+ if (!(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)))
+ {
+ log.Write(LogVerbosity.Warning, $"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;
@@ -270,7 +314,7 @@ namespace CryptoExchange.Net
log.Write(LogVerbosity.Debug, "Returned data: " + obj);
}
- private static PropertyInfo GetProperty(string name, IEnumerable props)
+ private static PropertyInfo? GetProperty(string name, IEnumerable props)
{
foreach (var prop in props)
{
diff --git a/CryptoExchange.Net/Converters/ArrayConverter.cs b/CryptoExchange.Net/Converters/ArrayConverter.cs
index ce6dede..4d7b8bc 100644
--- a/CryptoExchange.Net/Converters/ArrayConverter.cs
+++ b/CryptoExchange.Net/Converters/ArrayConverter.cs
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.Converters
}
///
- public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ public override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (objectType == typeof(JToken))
return JToken.Load(reader);
@@ -31,7 +31,7 @@ namespace CryptoExchange.Net.Converters
return ParseObject(arr, result, objectType);
}
- private static object ParseObject(JArray arr, object result, Type objectType)
+ private static object? ParseObject(JArray arr, object result, Type objectType)
{
foreach (var property in objectType.GetProperties())
{
@@ -76,7 +76,7 @@ namespace CryptoExchange.Net.Converters
var converterAttribute = (JsonConverterAttribute)property.GetCustomAttribute(typeof(JsonConverterAttribute)) ?? (JsonConverterAttribute)property.PropertyType.GetCustomAttribute(typeof(JsonConverterAttribute));
var conversionAttribute = (JsonConversionAttribute)property.GetCustomAttribute(typeof(JsonConversionAttribute)) ?? (JsonConversionAttribute)property.PropertyType.GetCustomAttribute(typeof(JsonConversionAttribute));
- object value;
+ object? value;
if (converterAttribute != null)
{
value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)}});
@@ -133,7 +133,7 @@ namespace CryptoExchange.Net.Converters
while (arrayProp.Index != last + 1)
{
- writer.WriteValue((string)null);
+ writer.WriteValue((string?)null);
last += 1;
}
diff --git a/CryptoExchange.Net/Converters/BaseConverter.cs b/CryptoExchange.Net/Converters/BaseConverter.cs
index b458149..97f18d0 100644
--- a/CryptoExchange.Net/Converters/BaseConverter.cs
+++ b/CryptoExchange.Net/Converters/BaseConverter.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Newtonsoft.Json;
@@ -10,7 +11,7 @@ namespace CryptoExchange.Net.Converters
/// Base class for enum converters
///
/// Type of enum to convert
- public abstract class BaseConverter: JsonConverter
+ public abstract class BaseConverter: JsonConverter where T: struct
{
///
/// The enum->string mapping
@@ -38,7 +39,7 @@ namespace CryptoExchange.Net.Converters
}
///
- public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ public override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.Value == null)
return null;
@@ -69,7 +70,7 @@ namespace CryptoExchange.Net.Converters
return objectType == typeof(T) || Nullable.GetUnderlyingType(objectType) == typeof(T);
}
- private bool GetValue(string value, out T result)
+ private bool GetValue(string value, [NotNullWhen(false)]out T result)
{
var mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
if (!mapping.Equals(default(KeyValuePair)))
diff --git a/CryptoExchange.Net/Converters/TimestampConverter.cs b/CryptoExchange.Net/Converters/TimestampConverter.cs
index 6d08a0d..f1b9103 100644
--- a/CryptoExchange.Net/Converters/TimestampConverter.cs
+++ b/CryptoExchange.Net/Converters/TimestampConverter.cs
@@ -15,7 +15,7 @@ namespace CryptoExchange.Net.Converters
}
///
- public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ public override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.Value == null)
return null;
diff --git a/CryptoExchange.Net/Converters/TimestampNanoSecondsConverter.cs b/CryptoExchange.Net/Converters/TimestampNanoSecondsConverter.cs
index fb8205d..80727b6 100644
--- a/CryptoExchange.Net/Converters/TimestampNanoSecondsConverter.cs
+++ b/CryptoExchange.Net/Converters/TimestampNanoSecondsConverter.cs
@@ -17,7 +17,7 @@ namespace CryptoExchange.Net.Converters
}
///
- public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ public override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.Value == null)
return null;
diff --git a/CryptoExchange.Net/Converters/TimestampSecondsConverter.cs b/CryptoExchange.Net/Converters/TimestampSecondsConverter.cs
index c1dff7e..1b2264a 100644
--- a/CryptoExchange.Net/Converters/TimestampSecondsConverter.cs
+++ b/CryptoExchange.Net/Converters/TimestampSecondsConverter.cs
@@ -16,8 +16,11 @@ namespace CryptoExchange.Net.Converters
}
///
- public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ public override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
+ if (reader.Value == null)
+ return null;
+
if (reader.Value is double d)
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(d);
diff --git a/CryptoExchange.Net/Converters/UTCDateTimeConverter.cs b/CryptoExchange.Net/Converters/UTCDateTimeConverter.cs
index 5210ccf..9690f7f 100644
--- a/CryptoExchange.Net/Converters/UTCDateTimeConverter.cs
+++ b/CryptoExchange.Net/Converters/UTCDateTimeConverter.cs
@@ -15,7 +15,7 @@ namespace CryptoExchange.Net.Converters
}
///
- public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ public override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.Value == null)
return null;
diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj
index 44fa670..783ef3e 100644
--- a/CryptoExchange.Net/CryptoExchange.Net.csproj
+++ b/CryptoExchange.Net/CryptoExchange.Net.csproj
@@ -1,7 +1,7 @@
- netstandard2.0
-
+ netstandard2.1
+
CryptoExchange.Net
JKorf
@@ -12,7 +12,8 @@
en
true
2.1.8 - Added array serialization options for implementations
- 7.1
+ enable
+ 8.0
CryptoExchange.Net.xml
diff --git a/CryptoExchange.Net/CryptoExchange.Net.xml b/CryptoExchange.Net/CryptoExchange.Net.xml
index 482e013..0a8433e 100644
--- a/CryptoExchange.Net/CryptoExchange.Net.xml
+++ b/CryptoExchange.Net/CryptoExchange.Net.xml
@@ -54,6 +54,12 @@
The api key used for identification
The api secret used for signing
+
+
+ Copy the credentials
+
+
+
Create Api credentials providing a stream containing json data. The json data should include two values: apiKey and apiSecret
@@ -70,13 +76,6 @@
-
-
- Create a secure string from a string
-
-
-
-
Dispose
@@ -98,7 +97,7 @@
-
+
Add authentication to the parameter list
@@ -108,7 +107,7 @@
-
+
Add authentication to the header dictionary
@@ -185,6 +184,12 @@
The private key used for signing
+
+
+ Copy the private key
+
+
+
Dispose
@@ -237,12 +242,6 @@
-
-
- Configure the client using the provided options
-
- Options
-
Set the authentication provider
@@ -276,6 +275,15 @@
A specific serializer to use
+
+
+ Deserialize a stream into an object
+
+ The type to deserialize into
+ The stream to deserialize
+ A specific serializer to use
+
+
Generate a unique id
@@ -467,11 +475,11 @@
The source secure string
-
+
- Header collection to IEnumerable
+ Create a secure string from a string
-
+
@@ -518,65 +526,49 @@
Request interface
-
+
- The uri of the request
-
-
-
-
- The headers of the request
-
-
-
-
- The method of the request
-
-
-
-
- The timeout of the request
-
-
-
-
- Set a proxy
-
-
-
-
-
-
-
-
- Content type
+ Accept header
- String content
+ Content
-
+
- Accept
+ Headers
-
+
- Content length
+ Method
-
+
- Get the request stream
+ Uri
-
-
+
- Get the response object
+ Set byte content
+
+
+
+
+ Set string content
+
+
+
+
+
+
+ Get the response
+
+
@@ -584,13 +576,21 @@
Request factory interface
-
+
Create a request for an uri
+
+
+
+ Configure the requests created by this factory
+
+ Request timeout to use
+ Proxy settings to use
+
Response object interface
@@ -601,18 +601,22 @@
The response status code
+
+
+ Whether the status code indicates a success status
+
+
+
+
+ The response headers
+
+
Get the response stream
-
-
- Get the response headers
-
-
-
Close the response
@@ -883,16 +887,6 @@
Is open
-
-
- Should ping connecting
-
-
-
-
- Interval of pinging
-
-
Supported ssl protocols
@@ -1090,6 +1084,16 @@
The proxy login
The proxy password
+
+
+
+ Create new settings for a proxy
+
+ The proxy hostname/ip
+ The proxy port
+ The proxy login
+ The proxy password
+
Comparer for byte order
@@ -1147,7 +1151,7 @@
The response headers
-
+
ctor
@@ -1163,7 +1167,7 @@
-
+
Create an error result
@@ -1177,26 +1181,6 @@
Constants
-
-
- GET Http method
-
-
-
-
- POST Http method
-
-
-
-
- DELETE Http method
-
-
-
-
- PUT Http method
-
-
Json content type header
@@ -1428,6 +1412,16 @@
+
+
+ Cancellation requested
+
+
+
+
+ ctor
+
+
Base options
@@ -1443,6 +1437,9 @@
The log writers
+
+
+
Base for order book options
@@ -1464,26 +1461,38 @@
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.
+
+
+
Base client options
-
-
- The api credentials
-
-
The base address of the client
+
+
+ The api credentials
+
+
Proxy to use
+
+
+ ctor
+
+
+
+
+
+
Base for rest client options
@@ -1504,6 +1513,12 @@
The time the server has to respond to a request before timing out
+
+
+ ctor
+
+
+
Create a copy of the options
@@ -1511,6 +1526,9 @@
+
+
+
Base for socket client options
@@ -1542,6 +1560,12 @@
Setting this to a higher number increases subscription speed, but having more subscriptions on a single connection will also increase the amount of traffic on that single connection.
+
+
+ ctor
+
+
+
Create a copy of the options
@@ -1549,6 +1573,9 @@
+
+
+
Order book entry
@@ -1876,43 +1903,35 @@
Request object
-
+
Create request object for web request
+
-
-
-
-
-
-
-
-
-
-
+
-
+
-
+
@@ -1920,7 +1939,10 @@
WebRequest factory
-
+
+
+
+
@@ -1931,16 +1953,19 @@
-
-
- Create response for http web response
-
-
-
-
+
-
+
+
+
+
+
+ Create response for a http response message
+
+ The actual response
+
+
@@ -1998,12 +2023,6 @@
-
-
- Configure the client using the provided options
-
- Options
-
Adds a rate limiter to the client. There are 2 choices, the and the .
@@ -2027,26 +2046,28 @@
The roundtrip time of the ping request
-
+
Execute a request
The expected result type
The uri to send the request to
The method of the request
+ Cancellation token
The parameters of the request
Whether or not the request should be authenticated
Whether or not the resulting object should be checked for missing properties in the mapping (only outputs if log verbosity is Debug)
-
+
- Can be overridden to indicate if a response is an error response
+ Executes the request and returns the string result
- The received data
- True if error response
+ The request object to execute
+ Cancellation token
+
-
+
Creates a request object
@@ -2056,26 +2077,13 @@
Whether or not the request should be authenticated
-
-
- Writes the string data of the parameters to the request body stream
-
-
-
-
-
+
Writes the parameters of the request to the request object, either in the query string or the request body
-
-
-
- Executes the request and returns the string result
-
- The request object to execute
-
+
@@ -2166,12 +2174,6 @@
Client options
Authentication provider
-
-
- Configure the client using the provided options
-
- Options
-
Set a function to interpret the data, used when the data is received as bytes instead of a string
@@ -2536,12 +2538,19 @@
If the subscription has been confirmed
-
+
+
+ ctor
+
+
+
+
+
+
ctor
-
diff --git a/CryptoExchange.Net/ExtensionMethods.cs b/CryptoExchange.Net/ExtensionMethods.cs
index 8d2d2c2..b8133b4 100644
--- a/CryptoExchange.Net/ExtensionMethods.cs
+++ b/CryptoExchange.Net/ExtensionMethods.cs
@@ -47,7 +47,7 @@ namespace CryptoExchange.Net
///
///
///
- public static void AddOptionalParameter(this Dictionary parameters, string key, object value)
+ public static void AddOptionalParameter(this Dictionary parameters, string key, object? value)
{
if(value != null)
parameters.Add(key, value);
@@ -59,7 +59,7 @@ namespace CryptoExchange.Net
///
///
///
- public static void AddOptionalParameter(this Dictionary parameters, string key, string value)
+ public static void AddOptionalParameter(this Dictionary parameters, string key, string? value)
{
if (value != null)
parameters.Add(key, value);
@@ -127,20 +127,17 @@ namespace CryptoExchange.Net
}
///
- /// Header collection to IEnumerable
+ /// Create a secure string from a string
///
- ///
+ ///
///
- public static IEnumerable> ToIEnumerable(this WebHeaderCollection headers)
+ internal static SecureString ToSecureString(this string source)
{
- if (headers == null)
- return null;
-
- return Enumerable
- .Range(0, headers.Count)
- .SelectMany(i => headers.GetValues(i)
- .Select(v => Tuple.Create(headers.GetKey(i), v))
- );
+ var secureString = new SecureString();
+ foreach (var c in source)
+ secureString.AppendChar(c);
+ secureString.MakeReadOnly();
+ return secureString;
}
///
@@ -152,7 +149,7 @@ namespace CryptoExchange.Net
///
public static async Task WaitOneAsync(this WaitHandle handle, int millisecondsTimeout, CancellationToken cancellationToken)
{
- RegisteredWaitHandle registeredHandle = null;
+ RegisteredWaitHandle? registeredHandle = null;
CancellationTokenRegistration tokenRegistration = default;
try
{
@@ -192,7 +189,7 @@ namespace CryptoExchange.Net
///
///
///
- public static JToken ToJToken(this string stringData, Log log = null)
+ public static JToken? ToJToken(this string stringData, Log? log = null)
{
if (string.IsNullOrEmpty(stringData))
return null;
diff --git a/CryptoExchange.Net/Interfaces/IRequest.cs b/CryptoExchange.Net/Interfaces/IRequest.cs
index 6c42ca5..656a276 100644
--- a/CryptoExchange.Net/Interfaces/IRequest.cs
+++ b/CryptoExchange.Net/Interfaces/IRequest.cs
@@ -1,6 +1,7 @@
using System;
-using System.IO;
-using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces
@@ -11,56 +12,41 @@ namespace CryptoExchange.Net.Interfaces
public interface IRequest
{
///
- /// The uri of the request
+ /// Accept header
+ ///
+ string Accept { set; }
+ ///
+ /// Content
+ ///
+ string? Content { get; }
+ ///
+ /// Headers
+ ///
+ HttpRequestHeaders Headers { get; }
+ ///
+ /// Method
+ ///
+ HttpMethod Method { get; set; }
+ ///
+ /// Uri
///
Uri Uri { get; }
///
- /// The headers of the request
+ /// Set byte content
///
- WebHeaderCollection Headers { get; set; }
+ ///
+ void SetContent(byte[] data);
///
- /// The method of the request
+ /// Set string content
///
- string Method { get; set; }
+ ///
+ ///
+ void SetContent(string data, string contentType);
///
- /// The timeout of the request
- ///
- TimeSpan Timeout { get; set; }
- ///
- /// Set a proxy
- ///
- ///
- ///
- ///
- ///
- void SetProxy(string host, int port, string login, string password);
-
- ///
- /// Content type
- ///
- string ContentType { get; set; }
- ///
- /// String content
- ///
- string Content { get; set; }
- ///
- /// Accept
- ///
- string Accept { get; set; }
- ///
- /// Content length
- ///
- long ContentLength { get; set; }
-
- ///
- /// Get the request stream
+ /// Get the response
///
+ ///
///
- Task GetRequestStream();
- ///
- /// Get the response object
- ///
- ///
- Task GetResponse();
+ Task GetResponse(CancellationToken cancellationToken);
}
}
diff --git a/CryptoExchange.Net/Interfaces/IRequestFactory.cs b/CryptoExchange.Net/Interfaces/IRequestFactory.cs
index 193db1d..ceb7f8c 100644
--- a/CryptoExchange.Net/Interfaces/IRequestFactory.cs
+++ b/CryptoExchange.Net/Interfaces/IRequestFactory.cs
@@ -1,4 +1,8 @@
-namespace CryptoExchange.Net.Interfaces
+using CryptoExchange.Net.Objects;
+using System;
+using System.Net.Http;
+
+namespace CryptoExchange.Net.Interfaces
{
///
/// Request factory interface
@@ -8,8 +12,16 @@
///
/// Create a request for an uri
///
+ ///
///
///
- IRequest Create(string uri);
+ IRequest Create(HttpMethod method, string uri);
+
+ ///
+ /// Configure the requests created by this factory
+ ///
+ /// Request timeout to use
+ /// Proxy settings to use
+ void Configure(TimeSpan requestTimeout, ApiProxy? proxy);
}
}
diff --git a/CryptoExchange.Net/Interfaces/IResponse.cs b/CryptoExchange.Net/Interfaces/IResponse.cs
index cc32604..9ebb888 100644
--- a/CryptoExchange.Net/Interfaces/IResponse.cs
+++ b/CryptoExchange.Net/Interfaces/IResponse.cs
@@ -1,7 +1,7 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.IO;
using System.Net;
+using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces
{
@@ -14,16 +14,23 @@ namespace CryptoExchange.Net.Interfaces
/// The response status code
///
HttpStatusCode StatusCode { get; }
+
+ ///
+ /// Whether the status code indicates a success status
+ ///
+ bool IsSuccessStatusCode { get; }
+
+ ///
+ /// The response headers
+ ///
+ IEnumerable>> ResponseHeaders { get; }
+
///
/// Get the response stream
///
///
- Stream GetResponseStream();
- ///
- /// Get the response headers
- ///
- ///
- IEnumerable> GetResponseHeaders();
+ Task GetResponseStream();
+
///
/// Close the response
///
diff --git a/CryptoExchange.Net/Interfaces/IWebsocket.cs b/CryptoExchange.Net/Interfaces/IWebsocket.cs
index 6a97da1..ede336e 100644
--- a/CryptoExchange.Net/Interfaces/IWebsocket.cs
+++ b/CryptoExchange.Net/Interfaces/IWebsocket.cs
@@ -34,7 +34,7 @@ namespace CryptoExchange.Net.Interfaces
///
/// Origin
///
- string Origin { get; set; }
+ string? Origin { get; set; }
///
/// Reconnecting
///
@@ -42,11 +42,11 @@ namespace CryptoExchange.Net.Interfaces
///
/// Handler for byte data
///
- Func DataInterpreterBytes { get; set; }
+ Func? DataInterpreterBytes { get; set; }
///
/// Handler for string data
///
- Func DataInterpreterString { get; set; }
+ Func? DataInterpreterString { get; set; }
///
/// Socket url
///
@@ -64,14 +64,6 @@ namespace CryptoExchange.Net.Interfaces
///
bool IsOpen { get; }
///
- /// Should ping connecting
- ///
- bool PingConnection { get; set; }
- ///
- /// Interval of pinging
- ///
- TimeSpan PingInterval { get; set; }
- ///
/// Supported ssl protocols
///
SslProtocols SSLProtocols { get; set; }
diff --git a/CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs b/CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs
index 5caac53..97c6ce0 100644
--- a/CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs
+++ b/CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs
@@ -49,11 +49,8 @@ namespace CryptoExchange.Net.Logging
///
protected override void Dispose(bool disposing)
{
- lock (writeLock)
- {
- logWriter.Close();
- logWriter = null;
- }
+ lock (writeLock)
+ logWriter.Close();
}
}
}
diff --git a/CryptoExchange.Net/Objects/ApiProxy.cs b/CryptoExchange.Net/Objects/ApiProxy.cs
index b4db095..9b66e7f 100644
--- a/CryptoExchange.Net/Objects/ApiProxy.cs
+++ b/CryptoExchange.Net/Objects/ApiProxy.cs
@@ -1,4 +1,5 @@
using System;
+using System.Security;
namespace CryptoExchange.Net.Objects
{
@@ -19,25 +20,20 @@ namespace CryptoExchange.Net.Objects
///
/// The login of the proxy
///
- public string Login { get; }
+ public string? Login { get; }
///
/// The password of the proxy
///
- public string Password { get; }
+ public SecureString? Password { get; }
///
/// Create new settings for a proxy
///
/// The proxy hostname/ip
/// The proxy port
- public ApiProxy(string host, int port)
+ public ApiProxy(string host, int port): this(host, port, null, (SecureString?)null)
{
- if(string.IsNullOrEmpty(host) || port <= 0)
- throw new ArgumentException("Proxy host or port not filled");
-
- Host = host;
- Port = port;
}
///
@@ -48,11 +44,28 @@ namespace CryptoExchange.Net.Objects
/// The proxy port
/// The proxy login
/// The proxy password
- public ApiProxy(string host, int port, string login, string password) : this(host, port)
+ public ApiProxy(string host, int port, string? login, string? password) : this(host, port, login, password?.ToSecureString())
{
- if (string.IsNullOrEmpty(login) || string.IsNullOrEmpty(password))
- throw new ArgumentException("Proxy login or password not filled");
+ }
+ ///
+ ///
+ /// Create new settings for a proxy
+ ///
+ /// The proxy hostname/ip
+ /// The proxy port
+ /// The proxy login
+ /// The proxy password
+ public ApiProxy(string host, int port, string? login, SecureString? password)
+ {
+ if (string.IsNullOrEmpty(login))
+ throw new ArgumentException("Proxy login not provided");
+
+ if (!host.StartsWith("http"))
+ throw new ArgumentException("Proxy host should start with either http:// or https://");
+
+ Host = host;
+ Port = port;
Login = login;
Password = password;
}
diff --git a/CryptoExchange.Net/Objects/CallResult.cs b/CryptoExchange.Net/Objects/CallResult.cs
index 1085328..78044f4 100644
--- a/CryptoExchange.Net/Objects/CallResult.cs
+++ b/CryptoExchange.Net/Objects/CallResult.cs
@@ -1,5 +1,5 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Net;
namespace CryptoExchange.Net.Objects
@@ -17,7 +17,7 @@ namespace CryptoExchange.Net.Objects
///
/// An error if the call didn't succeed
///
- public Error Error { get; internal set; }
+ public Error? Error { get; internal set; }
///
/// Whether the call was successful
///
@@ -28,11 +28,20 @@ namespace CryptoExchange.Net.Objects
///
///
///
- public CallResult(T data, Error error)
+ public CallResult([AllowNull]T data, Error? error)
{
Data = data;
Error = error;
}
+
+ ///
+ /// Overwrite bool check so we can use if(callResult) instead of if(callResult.Success)
+ ///
+ ///
+ public static implicit operator bool(CallResult obj)
+ {
+ return !ReferenceEquals(obj, null) && obj.Success;
+ }
}
///
@@ -49,7 +58,7 @@ namespace CryptoExchange.Net.Objects
///
/// The response headers
///
- public IEnumerable> ResponseHeaders { get; set; }
+ public IEnumerable>>? ResponseHeaders { get; set; }
///
/// ctor
@@ -58,7 +67,7 @@ namespace CryptoExchange.Net.Objects
///
///
///
- public WebCallResult(HttpStatusCode? code, IEnumerable> responseHeaders, T data, Error error): base(data, error)
+ public WebCallResult(HttpStatusCode? code, IEnumerable>>? responseHeaders, [AllowNull] T data, Error? error): base(data, error)
{
ResponseHeaders = responseHeaders;
ResponseStatusCode = code;
@@ -81,7 +90,7 @@ namespace CryptoExchange.Net.Objects
///
///
///
- public static WebCallResult CreateErrorResult(HttpStatusCode? code, IEnumerable> responseHeaders, Error error)
+ public static WebCallResult CreateErrorResult(HttpStatusCode? code, IEnumerable>> responseHeaders, Error error)
{
return new WebCallResult(code, responseHeaders, default, error);
}
diff --git a/CryptoExchange.Net/Objects/Constants.cs b/CryptoExchange.Net/Objects/Constants.cs
index 55112fa..82e5af5 100644
--- a/CryptoExchange.Net/Objects/Constants.cs
+++ b/CryptoExchange.Net/Objects/Constants.cs
@@ -5,23 +5,6 @@
///
public class Constants
{
- ///
- /// GET Http method
- ///
- public const string GetMethod = "GET";
- ///
- /// POST Http method
- ///
- public const string PostMethod = "POST";
- ///
- /// DELETE Http method
- ///
- public const string DeleteMethod = "DELETE";
- ///
- /// PUT Http method
- ///
- public const string PutMethod = "PUT";
-
///
/// Json content type header
///
diff --git a/CryptoExchange.Net/Objects/Error.cs b/CryptoExchange.Net/Objects/Error.cs
index 29240b1..0836d3e 100644
--- a/CryptoExchange.Net/Objects/Error.cs
+++ b/CryptoExchange.Net/Objects/Error.cs
@@ -137,4 +137,15 @@
///
public RateLimitError(string message) : base(8, "Rate limit exceeded: " + message) { }
}
+
+ ///
+ /// Cancellation requested
+ ///
+ public class CancellationRequestedError : Error
+ {
+ ///
+ /// ctor
+ ///
+ public CancellationRequestedError() : base(9, "Cancellation requested") { }
+ }
}
diff --git a/CryptoExchange.Net/Objects/Options.cs b/CryptoExchange.Net/Objects/Options.cs
index 22c3729..e7e67c7 100644
--- a/CryptoExchange.Net/Objects/Options.cs
+++ b/CryptoExchange.Net/Objects/Options.cs
@@ -21,6 +21,12 @@ namespace CryptoExchange.Net.Objects
/// The log writers
///
public List LogWriters { get; set; } = new List { new DebugTextWriter() };
+
+ ///
+ public override string ToString()
+ {
+ return $"LogVerbosity: {LogVerbosity}, Writers: {LogWriters.Count}";
+ }
}
///
@@ -47,6 +53,12 @@ namespace CryptoExchange.Net.Objects
OrderBookName = name;
SequenceNumbersAreConsecutive = sequencesAreConsecutive;
}
+
+ ///
+ public override string ToString()
+ {
+ return $"{base.ToString()}, OrderBookName: {OrderBookName}, SequenceNumbersAreConsequtive: {SequenceNumbersAreConsecutive}";
+ }
}
///
@@ -54,21 +66,36 @@ namespace CryptoExchange.Net.Objects
///
public class ClientOptions : BaseOptions
{
-
- ///
- /// The api credentials
- ///
- public ApiCredentials ApiCredentials { get; set; }
-
///
/// The base address of the client
///
public string BaseAddress { get; set; }
+ ///
+ /// The api credentials
+ ///
+ public ApiCredentials? ApiCredentials { get; set; }
+
+
///
/// Proxy to use
///
- public ApiProxy Proxy { get; set; }
+ public ApiProxy? Proxy { get; set; }
+
+ ///
+ /// ctor
+ ///
+ ///
+ public ClientOptions(string baseAddress)
+ {
+ BaseAddress = baseAddress;
+ }
+
+ ///
+ public override string ToString()
+ {
+ return $"{base.ToString()}, Credentials: {(ApiCredentials == null ? "-": "Set")}, BaseAddress: {BaseAddress}, Proxy: {(Proxy == null? "-": Proxy.Host)}";
+ }
}
///
@@ -91,6 +118,14 @@ namespace CryptoExchange.Net.Objects
///
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(30);
+ ///
+ /// ctor
+ ///
+ ///
+ public RestClientOptions(string baseAddress): base(baseAddress)
+ {
+ }
+
///
/// Create a copy of the options
///
@@ -110,10 +145,16 @@ namespace CryptoExchange.Net.Objects
};
if (ApiCredentials != null)
- copy.ApiCredentials = new ApiCredentials(ApiCredentials.Key.GetString(), ApiCredentials.Secret.GetString());
+ copy.ApiCredentials = ApiCredentials.Copy();
return copy;
}
+
+ ///
+ public override string ToString()
+ {
+ return $"{base.ToString()}, RateLimitters: {RateLimiters.Count}, RateLimitBehaviour: {RateLimitingBehaviour}, RequestTimeout: {RequestTimeout.ToString("c")}";
+ }
}
///
@@ -146,6 +187,14 @@ namespace CryptoExchange.Net.Objects
///
public int? SocketSubscriptionsCombineTarget { get; set; }
+ ///
+ /// ctor
+ ///
+ ///
+ public SocketClientOptions(string baseAddress) : base(baseAddress)
+ {
+ }
+
///
/// Create a copy of the options
///
@@ -166,9 +215,15 @@ namespace CryptoExchange.Net.Objects
};
if (ApiCredentials != null)
- copy.ApiCredentials = new ApiCredentials(ApiCredentials.Key.GetString(), ApiCredentials.Secret.GetString());
+ copy.ApiCredentials = ApiCredentials.Copy();
return copy;
}
+
+ ///
+ public override string ToString()
+ {
+ return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, SocketResponseTimeout: {SocketResponseTimeout.ToString("c")}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}";
+ }
}
}
diff --git a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs
index 47671b2..65aab30 100644
--- a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs
+++ b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs
@@ -31,7 +31,7 @@ namespace CryptoExchange.Net.OrderBook
protected SortedList bids;
private OrderBookStatus status;
- private UpdateSubscription subscription;
+ private UpdateSubscription? subscription;
private readonly bool sequencesAreConsecutive;
private readonly string id;
///
@@ -71,11 +71,11 @@ namespace CryptoExchange.Net.OrderBook
///
/// Event when the state changes
///
- public event Action OnStatusChange;
+ public event Action? OnStatusChange;
///
/// Event when order book was updated. Be careful! It can generate a lot of events at high-liquidity markets
///
- public event Action OnOrderBookUpdate;
+ public event Action? OnOrderBookUpdate;
///
/// Timestamp of the last update
///
@@ -145,6 +145,12 @@ namespace CryptoExchange.Net.OrderBook
///
protected SymbolOrderBook(string symbol, OrderBookOptions options)
{
+ if (symbol == null)
+ throw new ArgumentNullException("symbol");
+
+ if (options == null)
+ throw new ArgumentNullException("options");
+
id = options.OrderBookName;
processBuffer = new List();
sequencesAreConsecutive = options.SequenceNumbersAreConsecutive;
@@ -173,7 +179,7 @@ namespace CryptoExchange.Net.OrderBook
{
Status = OrderBookStatus.Connecting;
var startResult = await DoStart().ConfigureAwait(false);
- if (!startResult.Success)
+ if (!startResult)
return new CallResult(false, startResult.Error);
subscription = startResult.Data;
@@ -202,7 +208,7 @@ namespace CryptoExchange.Net.OrderBook
return;
var resyncResult = DoResync().Result;
- success = resyncResult.Success;
+ success = resyncResult;
}
log.Write(LogVerbosity.Info, $"{id} order book {Symbol} successfully resynchronized");
@@ -222,7 +228,8 @@ namespace CryptoExchange.Net.OrderBook
public async Task StopAsync()
{
Status = OrderBookStatus.Disconnected;
- await subscription.Close().ConfigureAwait(false);
+ if(subscription != null)
+ await subscription.Close().ConfigureAwait(false);
}
///
@@ -303,7 +310,7 @@ namespace CryptoExchange.Net.OrderBook
{
// Out of sync
log.Write(LogVerbosity.Warning, $"{id} order book {Symbol} out of sync, reconnecting");
- subscription.Reconnect().Wait();
+ subscription!.Reconnect().Wait();
}
else
{
diff --git a/CryptoExchange.Net/RateLimiter/RateLimiterAPIKey.cs b/CryptoExchange.Net/RateLimiter/RateLimiterAPIKey.cs
index 2e8ea4f..4bd9af3 100644
--- a/CryptoExchange.Net/RateLimiter/RateLimiterAPIKey.cs
+++ b/CryptoExchange.Net/RateLimiter/RateLimiterAPIKey.cs
@@ -32,7 +32,7 @@ namespace CryptoExchange.Net.RateLimiter
///
public CallResult LimitRequest(RestClient client, string url, RateLimitingBehaviour limitBehaviour)
{
- if(client.authProvider?.Credentials == null)
+ if(client.authProvider?.Credentials?.Key == null)
return new CallResult(0, null);
var key = client.authProvider.Credentials.Key.GetString();
diff --git a/CryptoExchange.Net/Requests/Request.cs b/CryptoExchange.Net/Requests/Request.cs
index 611f38c..4edbd5f 100644
--- a/CryptoExchange.Net/Requests/Request.cs
+++ b/CryptoExchange.Net/Requests/Request.cs
@@ -1,6 +1,8 @@
using System;
-using System.IO;
-using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces;
@@ -9,84 +11,61 @@ namespace CryptoExchange.Net.Requests
///
/// Request object
///
- public class Request : IRequest
+ internal class Request : IRequest
{
- private readonly WebRequest request;
+ private readonly HttpRequestMessage request;
+ private readonly HttpClient httpClient;
///
/// Create request object for web request
///
///
- public Request(WebRequest request)
+ ///
+ public Request(HttpRequestMessage request, HttpClient client)
{
+ httpClient = client;
this.request = request;
}
///
- public WebHeaderCollection Headers
- {
- get => request.Headers;
- set => request.Headers = value;
- }
+ public HttpRequestHeaders Headers => request.Headers;
///
- public string ContentType
- {
- get => request.ContentType;
- set => request.ContentType = value;
- }
-
- ///
- public string Content { get; set; }
+ public string? Content { get; private set; }
///
public string Accept
{
- get => ((HttpWebRequest)request).Accept;
- set => ((HttpWebRequest)request).Accept = value;
+ set => request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(value));
}
///
- public long ContentLength
- {
- get => ((HttpWebRequest)request).ContentLength;
- set => ((HttpWebRequest)request).ContentLength = value;
- }
-
- ///
- public string Method
+ public HttpMethod Method
{
get => request.Method;
set => request.Method = value;
}
- ///
- public TimeSpan Timeout
- {
- get => TimeSpan.FromMilliseconds(request.Timeout);
- set => request.Timeout = (int)Math.Round(value.TotalMilliseconds);
- }
-
///
public Uri Uri => request.RequestUri;
///
- public void SetProxy(string host, int port, string login, string password)
+ public void SetContent(string data, string contentType)
{
- request.Proxy = new WebProxy(host, port);
- if(!string.IsNullOrEmpty(login) && !string.IsNullOrEmpty(password)) request.Proxy.Credentials = new NetworkCredential(login, password);
+ Content = data;
+ request.Content = new StringContent(data, Encoding.UTF8, contentType);
}
///
- public async Task GetRequestStream()
+ public void SetContent(byte[] data)
{
- return await request.GetRequestStreamAsync().ConfigureAwait(false);
+ request.Content = new ByteArrayContent(data);
}
///
- public async Task GetResponse()
+ public async Task GetResponse(CancellationToken cancellationToken)
{
- return new Response((HttpWebResponse)await request.GetResponseAsync().ConfigureAwait(false));
+ return new Response(await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false));
}
}
}
diff --git a/CryptoExchange.Net/Requests/RequestFactory.cs b/CryptoExchange.Net/Requests/RequestFactory.cs
index add3f81..b91e188 100644
--- a/CryptoExchange.Net/Requests/RequestFactory.cs
+++ b/CryptoExchange.Net/Requests/RequestFactory.cs
@@ -1,5 +1,8 @@
-using System.Net;
+using System;
+using System.Net;
+using System.Net.Http;
using CryptoExchange.Net.Interfaces;
+using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net.Requests
{
@@ -8,10 +11,31 @@ namespace CryptoExchange.Net.Requests
///
public class RequestFactory : IRequestFactory
{
+ private HttpClient? httpClient;
+
///
- public IRequest Create(string uri)
+ public void Configure(TimeSpan requestTimeout, ApiProxy? proxy)
{
- return new Request(WebRequest.Create(uri));
+ HttpMessageHandler handler = new HttpClientHandler()
+ {
+ Proxy = proxy == null ? null : new WebProxy
+ {
+ Address = new Uri($"{proxy.Host}:{proxy.Port}"),
+ Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
+ }
+ };
+
+ httpClient = new HttpClient(handler);
+ httpClient.Timeout = requestTimeout;
+ }
+
+ ///
+ public IRequest Create(HttpMethod method, string uri)
+ {
+ if (httpClient == null)
+ throw new InvalidOperationException("Cant create request before configuring http client");
+
+ return new Request(new HttpRequestMessage(method, uri), httpClient);
}
}
}
diff --git a/CryptoExchange.Net/Requests/Response.cs b/CryptoExchange.Net/Requests/Response.cs
index a5d88a1..3df463d 100644
--- a/CryptoExchange.Net/Requests/Response.cs
+++ b/CryptoExchange.Net/Requests/Response.cs
@@ -1,7 +1,8 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.IO;
using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces;
namespace CryptoExchange.Net.Requests
@@ -9,38 +10,38 @@ namespace CryptoExchange.Net.Requests
///
/// HttpWebResponse response object
///
- public class Response : IResponse
+ internal class Response : IResponse
{
- private readonly HttpWebResponse response;
+ private readonly HttpResponseMessage response;
///
public HttpStatusCode StatusCode => response.StatusCode;
+ ///
+ public bool IsSuccessStatusCode => response.IsSuccessStatusCode;
+
+ ///
+ public IEnumerable>> ResponseHeaders => response.Headers;
+
///
- /// Create response for http web response
+ /// Create response for a http response message
///
- ///
- public Response(HttpWebResponse response)
+ /// The actual response
+ public Response(HttpResponseMessage response)
{
this.response = response;
}
///
- public Stream GetResponseStream()
+ public async Task GetResponseStream()
{
- return response.GetResponseStream();
- }
-
- ///
- public IEnumerable> GetResponseHeaders()
- {
- return response.Headers.ToIEnumerable();
+ return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
}
///
public void Close()
{
- response.Close();
+ response.Dispose();
}
}
}
diff --git a/CryptoExchange.Net/RestClient.cs b/CryptoExchange.Net/RestClient.cs
index c73f25b..a635a73 100644
--- a/CryptoExchange.Net/RestClient.cs
+++ b/CryptoExchange.Net/RestClient.cs
@@ -2,10 +2,10 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Net;
+using System.Net.Http;
using System.Net.NetworkInformation;
using System.Net.Sockets;
-using System.Text;
+using System.Threading;
using System.Threading.Tasks;
using System.Web;
using CryptoExchange.Net.Authentication;
@@ -29,7 +29,6 @@ namespace CryptoExchange.Net
///
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
-
///
/// Where to place post parameters
///
@@ -66,18 +65,13 @@ namespace CryptoExchange.Net
///
///
///
- protected RestClient(RestClientOptions exchangeOptions, AuthenticationProvider authenticationProvider): base(exchangeOptions, authenticationProvider)
+ protected RestClient(RestClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider): base(exchangeOptions, authenticationProvider)
{
- Configure(exchangeOptions);
- }
+ if (exchangeOptions == null)
+ throw new ArgumentNullException("Options");
- ///
- /// Configure the client using the provided options
- ///
- /// Options
- protected void Configure(RestClientOptions exchangeOptions)
- {
RequestTimeout = exchangeOptions.RequestTimeout;
+ RequestFactory.Configure(exchangeOptions.RequestTimeout, exchangeOptions.Proxy);
RateLimitBehaviour = exchangeOptions.RateLimitingBehaviour;
var rateLimiters = new List();
foreach (var rateLimiter in exchangeOptions.RateLimiters)
@@ -91,6 +85,9 @@ namespace CryptoExchange.Net
/// The limiter to add
public void AddRateLimiter(IRateLimiter limiter)
{
+ if (limiter == null)
+ throw new ArgumentNullException("limiter");
+
var rateLimiters = RateLimiters.ToList();
rateLimiters.Add(limiter);
RateLimiters = rateLimiters;
@@ -132,6 +129,10 @@ namespace CryptoExchange.Net
return new CallResult(0, new CantConnectError { Message = "Ping failed: " + exception.SocketErrorCode });
return new CallResult(0, new CantConnectError { Message = "Ping failed: " + e.InnerException.Message });
}
+ finally
+ {
+ ping.Dispose();
+ }
return reply.Status == IPStatus.Success ? new CallResult(reply.RoundtripTime, null) : new CallResult(0, new CantConnectError { Message = "Ping failed: " + reply.Status });
}
@@ -142,11 +143,13 @@ namespace CryptoExchange.Net
/// The expected result type
/// The uri to send the request to
/// The method of the request
+ /// Cancellation token
/// The parameters of the request
/// Whether or not the request should be authenticated
/// Whether or not the resulting object should be checked for missing properties in the mapping (only outputs if log verbosity is Debug)
///
- protected virtual async Task> ExecuteRequest(Uri uri, string method = Constants.GetMethod, Dictionary parameters = null, bool signed = false, bool checkResult = true) where T : class
+ protected virtual async Task> SendRequest(Uri uri, HttpMethod method, CancellationToken cancellationToken,
+ Dictionary? parameters = null, bool signed = false, bool checkResult = true) where T : class
{
log.Write(LogVerbosity.Debug, "Creating request for " + uri);
if (signed && authProvider == null)
@@ -156,13 +159,6 @@ namespace CryptoExchange.Net
}
var request = ConstructRequest(uri, method, parameters, signed);
-
- if (apiProxy != null)
- {
- log.Write(LogVerbosity.Debug, "Setting proxy");
- request.SetProxy(apiProxy.Host, apiProxy.Port, apiProxy.Login, apiProxy.Password);
- }
-
foreach (var limiter in RateLimiters)
{
var limitResult = limiter.LimitRequest(this, uri.AbsolutePath, RateLimitBehaviour);
@@ -176,37 +172,64 @@ namespace CryptoExchange.Net
log.Write(LogVerbosity.Debug, $"Request {uri.AbsolutePath} was limited by {limitResult.Data}ms by {limiter.GetType().Name}");
}
- string paramString = null;
- if (parameters != null && method == Constants.PostMethod)
- paramString = "with request body " + request.Content;
+ string? paramString = null;
+ if (parameters != null && method == HttpMethod.Post)
+ paramString = " with request body " + request.Content;
- log.Write(LogVerbosity.Debug, $"Sending {method}{(signed ? " signed" : "")} request to {request.Uri} {paramString ?? ""}");
- var result = await ExecuteRequest(request).ConfigureAwait(false);
- if(!result.Success)
- return new WebCallResult(result.ResponseStatusCode, result.ResponseHeaders, null, result.Error);
-
- var jsonResult = ValidateJson(result.Data);
- if(!jsonResult.Success)
- return new WebCallResult(result.ResponseStatusCode, result.ResponseHeaders, null, jsonResult.Error);
-
- if (IsErrorResponse(jsonResult.Data))
- return new WebCallResult(result.ResponseStatusCode, result.ResponseHeaders, null, ParseErrorResponse(jsonResult.Data));
-
- var desResult = Deserialize(jsonResult.Data, checkResult);
- if (!desResult.Success)
- return new WebCallResult(result.ResponseStatusCode, result.ResponseHeaders, null, desResult.Error);
-
- return new WebCallResult(result.ResponseStatusCode, result.ResponseHeaders, desResult.Data, null);
+ log.Write(LogVerbosity.Debug, $"Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(apiProxy == null? "": $" via proxy {apiProxy.Host}")}");
+ return await GetResponse(request, cancellationToken).ConfigureAwait(false);
}
///
- /// Can be overridden to indicate if a response is an error response
+ /// Executes the request and returns the string result
///
- /// The received data
- /// True if error response
- protected virtual bool IsErrorResponse(JToken data)
+ /// The request object to execute
+ /// Cancellation token
+ ///
+ private async Task> GetResponse(IRequest request, CancellationToken cancellationToken)
{
- return false;
+ try
+ {
+ TotalRequestsMade++;
+ var response = await request.GetResponse(cancellationToken).ConfigureAwait(false);
+ var statusCode = response.StatusCode;
+ var headers = response.ResponseHeaders;
+ var responseStream = await response.GetResponseStream().ConfigureAwait(false);
+ if (response.IsSuccessStatusCode)
+ {
+ var desResult = await Deserialize(responseStream).ConfigureAwait(false);
+ response.Close();
+ return new WebCallResult(statusCode, headers, desResult.Data, desResult.Error);
+ }
+ else
+ {
+ using var reader = new StreamReader(responseStream);
+ var data = await reader.ReadToEndAsync().ConfigureAwait(false);
+ response.Close();
+ var parseResult = ValidateJson(data);
+ return new WebCallResult(statusCode, headers, default, parseResult.Success ? ParseErrorResponse(parseResult.Data) :new ServerError(data));
+ }
+ }
+ catch (HttpRequestException requestException)
+ {
+ log.Write(LogVerbosity.Warning, "Request exception: " + requestException.Message);
+ return new WebCallResult(null, null, default, new ServerError(requestException.Message));
+ }
+ catch (TaskCanceledException canceledException)
+ {
+ if(canceledException.CancellationToken == cancellationToken)
+ {
+ // Cancellation token cancelled
+ log.Write(LogVerbosity.Warning, "Request cancel requested");
+ return new WebCallResult(null, null, default, new CancellationRequestedError());
+ }
+ else
+ {
+ // Request timed out
+ log.Write(LogVerbosity.Warning, "Request timed out");
+ return new WebCallResult(null, null, default, new WebError("Request timed out"));
+ }
+ }
}
///
@@ -217,7 +240,7 @@ namespace CryptoExchange.Net
/// The parameters of the request
/// Whether or not the request should be authenticated
///
- protected virtual IRequest ConstructRequest(Uri uri, string method, Dictionary parameters, bool signed)
+ protected virtual IRequest ConstructRequest(Uri uri, HttpMethod method, Dictionary? parameters, bool signed)
{
if (parameters == null)
parameters = new Dictionary();
@@ -226,57 +249,44 @@ namespace CryptoExchange.Net
if(authProvider != null)
parameters = authProvider.AddAuthenticationToParameters(uriString, method, parameters, signed);
- if((method == Constants.GetMethod || method == Constants.DeleteMethod || postParametersPosition == PostParameters.InUri) && parameters?.Any() == true)
+ if((method == HttpMethod.Get || method == HttpMethod.Delete || postParametersPosition == PostParameters.InUri) && parameters?.Any() == true)
uriString += "?" + parameters.CreateParamString(true, arraySerialization);
-
- var request = RequestFactory.Create(uriString);
- request.ContentType = requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
+
+ var contentType = requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
+ var request = RequestFactory.Create(method, uriString);
request.Accept = Constants.JsonContentHeader;
- request.Method = method;
var headers = new Dictionary();
if (authProvider != null)
- headers = authProvider.AddAuthenticationToHeaders(uriString, method, parameters, signed);
+ headers = authProvider.AddAuthenticationToHeaders(uriString, method, parameters!, signed);
foreach (var header in headers)
request.Headers.Add(header.Key, header.Value);
- if ((method == Constants.PostMethod || method == Constants.PutMethod) && postParametersPosition != PostParameters.InUri)
+ if ((method == HttpMethod.Post || method == HttpMethod.Put) && postParametersPosition != PostParameters.InUri)
{
if(parameters?.Any() == true)
- WriteParamBody(request, parameters);
- else
- WriteParamBody(request, "{}");
+ WriteParamBody(request, parameters, contentType);
+ else
+ request.SetContent("{}", contentType);
}
return request;
}
- ///
- /// Writes the string data of the parameters to the request body stream
- ///
- ///
- ///
- protected virtual void WriteParamBody(IRequest request, string stringData)
- {
- var data = Encoding.UTF8.GetBytes(stringData);
- request.ContentLength = data.Length;
- request.Content = stringData;
- using (var stream = request.GetRequestStream().Result)
- stream.Write(data, 0, data.Length);
- }
-
///
/// Writes the parameters of the request to the request object, either in the query string or the request body
///
///
///
- protected virtual void WriteParamBody(IRequest request, Dictionary parameters)
+ ///
+ protected virtual void WriteParamBody(IRequest request, Dictionary parameters, string contentType)
{
if (requestBodyFormat == RequestBodyFormat.Json)
{
var stringData = JsonConvert.SerializeObject(parameters.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value));
- WriteParamBody(request, stringData);
+ request.SetContent(stringData, contentType);
+
}
else if(requestBodyFormat == RequestBodyFormat.FormData)
{
@@ -284,81 +294,9 @@ namespace CryptoExchange.Net
foreach (var kvp in parameters.OrderBy(p => p.Key))
formData.Add(kvp.Key, kvp.Value.ToString());
var stringData = formData.ToString();
- WriteParamBody(request, stringData);
+ request.SetContent(stringData, contentType);
}
- }
-
- ///
- /// Executes the request and returns the string result
- ///
- /// The request object to execute
- ///
- private async Task> ExecuteRequest(IRequest request)
- {
- var returnedData = "";
- try
- {
- request.Timeout = RequestTimeout;
- TotalRequestsMade++;
- var response = await request.GetResponse().ConfigureAwait(false);
- using (var reader = new StreamReader(response.GetResponseStream()))
- {
- returnedData = await reader.ReadToEndAsync().ConfigureAwait(false);
- log.Write(LogVerbosity.Debug, "Data returned: " + returnedData);
- }
-
- var statusCode = response.StatusCode;
- var returnHeaders = response.GetResponseHeaders();
- response.Close();
- return new WebCallResult(statusCode, returnHeaders, returnedData, null);
- }
- catch (WebException we)
- {
- var response = (HttpWebResponse)we.Response;
- var statusCode = response?.StatusCode;
- var returnHeaders = response?.Headers.ToIEnumerable();
-
- try
- {
- var responseStream = response?.GetResponseStream();
- if (response != null && responseStream != null)
- {
- using (var reader = new StreamReader(responseStream))
- {
- returnedData = await reader.ReadToEndAsync().ConfigureAwait(false);
- log.Write(LogVerbosity.Warning, "Server returned an error: " + returnedData);
- }
-
- response.Close();
-
- var jsonResult = ValidateJson(returnedData);
- return !jsonResult.Success ? new WebCallResult(statusCode, returnHeaders, null, jsonResult.Error) : new WebCallResult(statusCode, returnHeaders, null, ParseErrorResponse(jsonResult.Data));
- }
- }
- catch (Exception e)
- {
- log.Write(LogVerbosity.Debug, "Not able to read server response: " + e.Message);
- }
-
- var infoMessage = "No response from server";
- if (response == null)
- {
- infoMessage += $" | {we.Status} - {we.Message}";
- log.Write(LogVerbosity.Warning, infoMessage);
- return new WebCallResult(0, null, null, new WebError(infoMessage));
- }
-
- infoMessage = $"Status: {response.StatusCode}-{response.StatusDescription}, Message: {we.Message}";
- log.Write(LogVerbosity.Warning, infoMessage);
- response.Close();
- return new WebCallResult(statusCode, returnHeaders, null, new ServerError(infoMessage));
- }
- catch (Exception e)
- {
- log.Write(LogVerbosity.Error, $"Unknown error occured: {e.GetType()}, {e.Message}, {e.StackTrace}");
- return new WebCallResult(null, null, null, new UnknownError(e.Message + ", data: " + returnedData));
- }
- }
+ }
///
/// Parse an error response from the server. Only used when server returns a status other than Success(200)
diff --git a/CryptoExchange.Net/SocketClient.cs b/CryptoExchange.Net/SocketClient.cs
index f2f8afd..ef99cfb 100644
--- a/CryptoExchange.Net/SocketClient.cs
+++ b/CryptoExchange.Net/SocketClient.cs
@@ -50,11 +50,11 @@ namespace CryptoExchange.Net
///
/// Handler for byte data
///
- protected Func dataInterpreterBytes;
+ protected Func? dataInterpreterBytes;
///
/// Handler for string data
///
- protected Func dataInterpreterString;
+ protected Func? dataInterpreterString;
///
/// Generic handlers
///
@@ -62,11 +62,11 @@ namespace CryptoExchange.Net
///
/// Periodic task
///
- protected Task periodicTask;
+ protected Task? periodicTask;
///
/// Periodic task event
///
- protected AutoResetEvent periodicEvent;
+ protected AutoResetEvent? periodicEvent;
///
/// Is disposing
///
@@ -84,17 +84,11 @@ namespace CryptoExchange.Net
///
/// Client options
/// Authentication provider
- protected SocketClient(SocketClientOptions exchangeOptions, AuthenticationProvider authenticationProvider): base(exchangeOptions, authenticationProvider)
+ protected SocketClient(SocketClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider): base(exchangeOptions, authenticationProvider)
{
- Configure(exchangeOptions);
- }
+ if (exchangeOptions == null)
+ throw new ArgumentNullException("Options");
- ///
- /// Configure the client using the provided options
- ///
- /// Options
- protected void Configure(SocketClientOptions exchangeOptions)
- {
AutoReconnect = exchangeOptions.AutoReconnect;
ReconnectInterval = exchangeOptions.ReconnectInterval;
ResponseTimeout = exchangeOptions.SocketResponseTimeout;
@@ -137,7 +131,7 @@ namespace CryptoExchange.Net
/// If the subscription should be authenticated
/// The handler of update data
///
- protected virtual async Task> Subscribe(string url, object request, string identifier, bool authenticated, Action dataHandler)
+ protected virtual async Task> Subscribe(string url, object? request, string? identifier, bool authenticated, Action dataHandler)
{
SocketConnection socket;
SocketSubscription handler;
@@ -155,7 +149,7 @@ namespace CryptoExchange.Net
}
var connectResult = await ConnectIfNeeded(socket, authenticated).ConfigureAwait(false);
- if (!connectResult.Success)
+ if (!connectResult)
return new CallResult(null, connectResult.Error);
}
finally
@@ -170,7 +164,7 @@ namespace CryptoExchange.Net
if (request != null)
{
var subResult = await SubscribeAndWait(socket, request, handler).ConfigureAwait(false);
- if (!subResult.Success)
+ if (!subResult)
{
await socket.Close(handler).ConfigureAwait(false);
return new CallResult(null, subResult.Error);
@@ -192,8 +186,8 @@ namespace CryptoExchange.Net
///
protected internal virtual async Task> SubscribeAndWait(SocketConnection socket, object request, SocketSubscription subscription)
{
- CallResult