diff --git a/CryptoExchange.Net/CryptoExchange.Net.xml b/CryptoExchange.Net/CryptoExchange.Net.xml index ed94e55..0f06bb5 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.xml +++ b/CryptoExchange.Net/CryptoExchange.Net.xml @@ -633,13 +633,13 @@ - + Configure the requests created by this factory Request timeout to use - Proxy settings to use - Should generate unique id for requests + Proxy settings to use + Optional shared http client instance @@ -1622,12 +1622,24 @@ The time the server has to respond to a request before timing out + + + http client + + ctor + + + ctor + + + Shared http client + Create a copy of the options @@ -2030,13 +2042,12 @@ Request object - + Create request object for web request - - if true, should assign unique id for request + @@ -2070,7 +2081,7 @@ WebRequest factory - + @@ -2958,5 +2969,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/IRequestFactory.cs b/CryptoExchange.Net/Interfaces/IRequestFactory.cs index 59b1bb8..e05b371 100644 --- a/CryptoExchange.Net/Interfaces/IRequestFactory.cs +++ b/CryptoExchange.Net/Interfaces/IRequestFactory.cs @@ -22,6 +22,7 @@ namespace CryptoExchange.Net.Interfaces /// /// Request timeout to use /// Proxy settings to use - void Configure(TimeSpan requestTimeout, ApiProxy? proxy); + /// Optional shared http client instance + void Configure(TimeSpan requestTimeout, ApiProxy? proxy, HttpClient? httpClient=null); } } diff --git a/CryptoExchange.Net/Objects/Options.cs b/CryptoExchange.Net/Objects/Options.cs index 577de1e..bb09671 100644 --- a/CryptoExchange.Net/Objects/Options.cs +++ b/CryptoExchange.Net/Objects/Options.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Net.Http; using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Logging; @@ -126,7 +127,10 @@ namespace CryptoExchange.Net.Objects /// The time the server has to respond to a request before timing out /// public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(30); - + /// + /// http client + /// + public HttpClient? HttpClient; /// /// ctor /// @@ -134,7 +138,15 @@ namespace CryptoExchange.Net.Objects public RestClientOptions(string baseAddress): base(baseAddress) { } - + /// + /// ctor + /// + /// + /// Shared http client + public RestClientOptions(HttpClient httpClient, string baseAddress) : base(baseAddress) + { + HttpClient = httpClient; + } /// /// Create a copy of the options /// @@ -150,7 +162,8 @@ namespace CryptoExchange.Net.Objects LogWriters = LogWriters, RateLimiters = RateLimiters, RateLimitingBehaviour = RateLimitingBehaviour, - RequestTimeout = RequestTimeout + RequestTimeout = RequestTimeout, + HttpClient = HttpClient }; if (ApiCredentials != null) diff --git a/CryptoExchange.Net/Requests/RequestFactory.cs b/CryptoExchange.Net/Requests/RequestFactory.cs index 54cd8a2..3502c61 100644 --- a/CryptoExchange.Net/Requests/RequestFactory.cs +++ b/CryptoExchange.Net/Requests/RequestFactory.cs @@ -11,21 +11,28 @@ namespace CryptoExchange.Net.Requests /// public class RequestFactory : IRequestFactory { - private HttpClient? httpClient; - private bool isTracingEnabled; - /// - public void Configure(TimeSpan requestTimeout, ApiProxy? proxy) - { - 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) - } - }; + private HttpClient? httpClient; - httpClient = new HttpClient(handler) { Timeout = requestTimeout }; + /// + public void Configure(TimeSpan requestTimeout, ApiProxy? proxy, HttpClient? client = null) + { + if (client == null) + { + 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) { Timeout = requestTimeout }; + } + else + { + httpClient = client; + } } /// diff --git a/CryptoExchange.Net/RestClient.cs b/CryptoExchange.Net/RestClient.cs index e9932d2..945ff79 100644 --- a/CryptoExchange.Net/RestClient.cs +++ b/CryptoExchange.Net/RestClient.cs @@ -24,13 +24,13 @@ namespace CryptoExchange.Net /// /// Base rest client /// - public abstract class RestClient: BaseClient, IRestClient + public abstract class RestClient : BaseClient, IRestClient { /// /// The factory for creating requests. Used for unit testing /// public IRequestFactory RequestFactory { get; set; } = new RequestFactory(); - + /// /// Where to place post parameters /// @@ -77,13 +77,13 @@ namespace CryptoExchange.Net /// /// /// - protected RestClient(RestClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider): base(exchangeOptions, authenticationProvider) + protected RestClient(RestClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider) : base(exchangeOptions, authenticationProvider) { if (exchangeOptions == null) throw new ArgumentNullException(nameof(exchangeOptions)); RequestTimeout = exchangeOptions.RequestTimeout; - RequestFactory.Configure(exchangeOptions.RequestTimeout, exchangeOptions.Proxy); + RequestFactory.Configure(exchangeOptions.RequestTimeout, exchangeOptions.Proxy, exchangeOptions.HttpClient); RateLimitBehaviour = exchangeOptions.RateLimitingBehaviour; var rateLimiters = new List(); foreach (var rateLimiter in exchangeOptions.RateLimiters) @@ -134,10 +134,10 @@ namespace CryptoExchange.Net { reply = await ping.SendPingAsync(uri.Host).ConfigureAwait(false); } - catch(PingException e) + catch (PingException e) { if (e.InnerException == null) - return new CallResult(0, new CantConnectError {Message = "Ping failed: " + e.Message}); + 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 }); @@ -149,7 +149,7 @@ namespace CryptoExchange.Net ping.Dispose(); } - if(ct.IsCancellationRequested) + 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 }); @@ -174,7 +174,7 @@ namespace CryptoExchange.Net { log.Write(LogVerbosity.Debug, "Creating request for " + uri); if (signed && authProvider == null) - { + { log.Write(LogVerbosity.Warning, $"Request {uri.AbsolutePath} failed because no ApiCredentials were provided"); return new WebCallResult(null, null, null, new NoApiCredentialsError()); } @@ -185,19 +185,19 @@ namespace CryptoExchange.Net var limitResult = limiter.LimitRequest(this, uri.AbsolutePath, RateLimitBehaviour); if (!limitResult.Success) { - log.Write(LogVerbosity.Debug, $"Request {uri.AbsolutePath} failed because of rate limit"); + log.Write(LogVerbosity.Debug, $"Request {request.RequestId} {uri.AbsolutePath} failed because of rate limit"); return new WebCallResult(null, null, null, limitResult.Error); } if (limitResult.Data > 0) - log.Write(LogVerbosity.Debug, $"Request {request.RequestId} {uri.AbsolutePath} was limited by {limitResult.Data}ms by {limiter.GetType().Name}"); + log.Write(LogVerbosity.Debug, $"Request {request.RequestId} {uri.AbsolutePath} was limited by {limitResult.Data}ms by {limiter.GetType().Name}"); } - string? paramString = null; - if (method == HttpMethod.Post) + string? paramString = null; + if (method == HttpMethod.Post) paramString = " with request body " + request.Content; - log.Write(LogVerbosity.Debug, $"Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(apiProxy == null? "": $" via proxy {apiProxy.Host}")} with id {request.RequestId}"); + log.Write(LogVerbosity.Debug, $"Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(apiProxy == null ? "" : $" via proxy {apiProxy.Host}")} with id {request.RequestId}"); return await GetResponse(request, cancellationToken).ConfigureAwait(false); } @@ -230,7 +230,7 @@ namespace CryptoExchange.Net if (!parseResult.Success) return WebCallResult.CreateErrorResult(response.StatusCode, response.ResponseHeaders, new ServerError(data)); var error = await TryParseError(parseResult.Data); - if(error != null) + if (error != null) return WebCallResult.CreateErrorResult(response.StatusCode, response.ResponseHeaders, error); var deserializeResult = Deserialize(parseResult.Data); @@ -253,7 +253,7 @@ namespace CryptoExchange.Net responseStream.Close(); response.Close(); var parseResult = ValidateJson(data); - return new WebCallResult(statusCode, headers, default, parseResult.Success ? ParseErrorResponse(parseResult.Data) :new ServerError(data)); + return new WebCallResult(statusCode, headers, default, parseResult.Success ? ParseErrorResponse(parseResult.Data) : new ServerError(data)); } } catch (HttpRequestException requestException) @@ -263,7 +263,7 @@ namespace CryptoExchange.Net } catch (TaskCanceledException canceledException) { - if(canceledException.CancellationToken == cancellationToken) + if (canceledException.CancellationToken == cancellationToken) { // Cancellation token cancelled log.Write(LogVerbosity.Warning, $"Request {request.RequestId} cancel requested"); @@ -305,10 +305,10 @@ namespace CryptoExchange.Net parameters = new Dictionary(); var uriString = uri.ToString(); - if(authProvider != null) + if (authProvider != null) parameters = authProvider.AddAuthenticationToParameters(uriString, method, parameters, signed, postPosition, arraySerialization); - if((method == HttpMethod.Get || method == HttpMethod.Delete || postPosition == PostParameters.InUri) && parameters?.Any() == true) + if ((method == HttpMethod.Get || method == HttpMethod.Delete || postPosition == PostParameters.InUri) && parameters?.Any() == true) uriString += "?" + parameters.CreateParamString(true, arraySerialization); var contentType = requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader; @@ -324,7 +324,7 @@ namespace CryptoExchange.Net if ((method == HttpMethod.Post || method == HttpMethod.Put) && postPosition != PostParameters.InUri) { - if(parameters?.Any() == true) + if (parameters?.Any() == true) WriteParamBody(request, parameters, contentType); else request.SetContent(requestBodyEmptyContent, contentType); @@ -346,7 +346,7 @@ namespace CryptoExchange.Net var stringData = JsonConvert.SerializeObject(parameters.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value)); request.SetContent(stringData, contentType); } - else if(requestBodyFormat == RequestBodyFormat.FormData) + else if (requestBodyFormat == RequestBodyFormat.FormData) { var formData = HttpUtility.ParseQueryString(string.Empty); foreach (var kvp in parameters.OrderBy(p => p.Key)) @@ -354,7 +354,7 @@ namespace CryptoExchange.Net if (kvp.Value.GetType().IsArray) { var array = (Array)kvp.Value; - foreach(var value in array) + foreach (var value in array) formData.Add(kvp.Key, value.ToString()); } else @@ -363,7 +363,7 @@ namespace CryptoExchange.Net var stringData = formData.ToString(); request.SetContent(stringData, contentType); } - } + } /// /// Parse an error response from the server. Only used when server returns a status other than Success(200)