mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2025-12-17 11:37:33 +00:00
Performance update: Authentication Added Ed25519 signing support for NET8.0 and newer Added static methods on ApiCredentials to create credentials of a specific type Added static ApiCredentials.ReadFromFile method to read a key from file Added required abstract SupportedCredentialTypes property on AuthenticationProvider base class General Performance Added checks before logging statements to prevent overhead of building the log string if logging is not needed Added ExchangeHelpers.ProcessQueuedAsync method to process updates async Replaced locking object types from object to Lock in NET9.0 and newer Replaced some Task response types with ValueTask to prevent allocation overhead on hot paths Updated Json ArrayConverter to reduce some allocation overhead Updated Json BoolConverter to prevent boxing Updated Json DateTimeConverter to prevent boxing Updated Json EnumConverter caching to reduce lookup overhead Updated ExtensionMethods.CreateParamString to reduce allocations Updated ExtensionMethods.AppendPath to reduce overhead REST Refactored REST message processing to separate IRestMessageHandler instance Split RestApiClient.PrepareAsync into CheckTimeSync and RateLimitAsync Updated IRequest.Accept type from string to MediaTypeWithQualityHeaderValue to prevent creation on each request Updated IRequest.GetHeaders response type from KeyValuePair<string, string[]>[] to HttpRequestHeaders to prevent additional mapping Updated IResponse.ResponseHeaders type from KeyValuePair<string, string[]>[] to HttpResponseHeaders to prevent additional mapping Updated WebCallResult RequestHeaders and ResponseHeaders types to HttpRequestHeaders and HttpResponseHeaders Removed unnecessary empty dictionary initializations for each request Removed CallResult creation in internal methods to prevent having to create multiple versions for different result types Socket Added HighPerformance websocket client implementation which significantly reduces memory overhead and improves speed but with certain limitations Added MaxIndividualSubscriptionsPerConnection setting in SocketApiClient to limit the number of individual stream subscriptions on a connection Added SocketIndividualSubscriptionCombineTarget option to set the target number of individual stream subscriptions per connection Added new websocket message handling logic which is faster and reduces memory allocation Added UseUpdatedDeserialization option to toggle between updated deserialization and old deserialization Added Exchange property to DataEvent to prevent additional mapping overhead for Shared apis Refactored message callback to be sync instead of async to prevent async overhead Refactored CryptoExchangeWebSocketClient.IncomingKbps calculation to significantly reduce overhead Moved websocket client creation from SocketApiClient to SocketConnection Removed DataEvent.As and DataEvent.ToCallResult methods in favor of single ToType method Removed DataEvent creation on lower levels to prevent having to create multiple versions for different result types Removed Subscription<TSubResponse, TUnsubResponse> as its no longer used Other Added null check to ParameterCollection for required parameters Added Net10.0 target framework Updated dependency versions Updated Shared asset aliases check to be culture invariant Updated Error string representation Updated some namespaces Updated SymbolOrderBook processing of buffered updates to prevent additional allocation Removed ExchangeEvent type which is no longer needed Removed unused usings
235 lines
11 KiB
C#
235 lines
11 KiB
C#
using CryptoExchange.Net.Interfaces;
|
|
using CryptoExchange.Net.Objects;
|
|
using Moq;
|
|
using System;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using CryptoExchange.Net.Authentication;
|
|
using System.Collections.Generic;
|
|
using Microsoft.Extensions.Logging;
|
|
using CryptoExchange.Net.Clients;
|
|
using Microsoft.Extensions.Options;
|
|
using System.Linq;
|
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
|
using System.Text.Json.Serialization;
|
|
using System.Net.Http.Headers;
|
|
using CryptoExchange.Net.SharedApis;
|
|
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
|
|
|
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|
{
|
|
public class TestRestClient: BaseRestClient
|
|
{
|
|
public TestRestApi1Client Api1 { get; }
|
|
public TestRestApi2Client Api2 { get; }
|
|
|
|
public TestRestClient(Action<TestClientOptions> optionsDelegate = null)
|
|
: this(null, null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
|
|
{
|
|
}
|
|
|
|
public TestRestClient(HttpClient httpClient, ILoggerFactory loggerFactory, IOptions<TestClientOptions> options) : base(loggerFactory, "Test")
|
|
{
|
|
Initialize(options.Value);
|
|
|
|
Api1 = new TestRestApi1Client(options.Value);
|
|
Api2 = new TestRestApi2Client(options.Value);
|
|
}
|
|
|
|
public void SetResponse(string responseData, out IRequest requestObj)
|
|
{
|
|
var expectedBytes = Encoding.UTF8.GetBytes(responseData);
|
|
var responseStream = new MemoryStream();
|
|
responseStream.Write(expectedBytes, 0, expectedBytes.Length);
|
|
responseStream.Seek(0, SeekOrigin.Begin);
|
|
|
|
var response = new Mock<IResponse>();
|
|
response.Setup(c => c.IsSuccessStatusCode).Returns(true);
|
|
response.Setup(c => c.GetResponseStreamAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult((Stream)responseStream));
|
|
|
|
var headers = new HttpRequestMessage().Headers;
|
|
var request = new Mock<IRequest>();
|
|
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
|
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
|
request.Setup(c => c.SetContent(It.IsAny<string>(), It.IsAny<string>())).Callback(new Action<string, string>((content, type) => { request.Setup(r => r.Content).Returns(content); }));
|
|
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new string[] { val }));
|
|
request.Setup(c => c.GetHeaders()).Returns(() => headers);
|
|
|
|
var factory = Mock.Get(Api1.RequestFactory);
|
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
|
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
|
|
{
|
|
request.Setup(a => a.Uri).Returns(uri);
|
|
request.Setup(a => a.Method).Returns(method);
|
|
})
|
|
.Returns(request.Object);
|
|
|
|
factory = Mock.Get(Api2.RequestFactory);
|
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
|
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
|
|
{
|
|
request.Setup(a => a.Uri).Returns(uri);
|
|
request.Setup(a => a.Method).Returns(method);
|
|
})
|
|
.Returns(request.Object);
|
|
requestObj = request.Object;
|
|
}
|
|
|
|
public void SetErrorWithoutResponse(HttpStatusCode code, string message)
|
|
{
|
|
var we = new HttpRequestException();
|
|
typeof(HttpRequestException).GetField("_message", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(we, message);
|
|
|
|
var request = new Mock<IRequest>();
|
|
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
|
request.Setup(c => c.GetHeaders()).Returns(new HttpRequestMessage().Headers);
|
|
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
|
|
|
|
var factory = Mock.Get(Api1.RequestFactory);
|
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
|
.Returns(request.Object);
|
|
|
|
|
|
factory = Mock.Get(Api2.RequestFactory);
|
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
|
.Returns(request.Object);
|
|
}
|
|
|
|
public void SetErrorWithResponse(string responseData, HttpStatusCode code)
|
|
{
|
|
var expectedBytes = Encoding.UTF8.GetBytes(responseData);
|
|
var responseStream = new MemoryStream();
|
|
responseStream.Write(expectedBytes, 0, expectedBytes.Length);
|
|
responseStream.Seek(0, SeekOrigin.Begin);
|
|
|
|
var response = new Mock<IResponse>();
|
|
response.Setup(c => c.IsSuccessStatusCode).Returns(false);
|
|
response.Setup(c => c.GetResponseStreamAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult((Stream)responseStream));
|
|
|
|
var headers = new List<KeyValuePair<string, string[]>>();
|
|
var request = new Mock<IRequest>();
|
|
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
|
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
|
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(new KeyValuePair<string, string[]>(key, new string[] { val })));
|
|
request.Setup(c => c.GetHeaders()).Returns(new HttpRequestMessage().Headers);
|
|
|
|
var factory = Mock.Get(Api1.RequestFactory);
|
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
|
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
|
.Returns(request.Object);
|
|
|
|
factory = Mock.Get(Api2.RequestFactory);
|
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
|
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
|
.Returns(request.Object);
|
|
}
|
|
}
|
|
|
|
public class TestRestApi1Client : RestApiClient
|
|
{
|
|
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
|
|
|
|
public TestRestApi1Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api1Options)
|
|
{
|
|
RequestFactory = new Mock<IRequestFactory>().Object;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
|
|
|
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions() { TypeInfoResolver = new TestSerializerContext() });
|
|
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
|
|
|
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
|
{
|
|
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
|
|
}
|
|
|
|
public async Task<CallResult<T>> RequestWithParams<T>(HttpMethod method, ParameterCollection parameters, Dictionary<string, string> headers) where T : class
|
|
{
|
|
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", method) { Weight = 0 }, parameters, default, additionalHeaders: headers);
|
|
}
|
|
|
|
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
|
|
{
|
|
ParameterPositions[method] = position;
|
|
}
|
|
|
|
public override TimeSpan? GetTimeOffset()
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
|
=> new TestAuthProvider(credentials);
|
|
|
|
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync()
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
public override TimeSyncInfo GetTimeSyncInfo()
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
}
|
|
|
|
public class TestRestApi2Client : RestApiClient
|
|
{
|
|
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
|
|
|
|
public TestRestApi2Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api2Options)
|
|
{
|
|
RequestFactory = new Mock<IRequestFactory>().Object;
|
|
}
|
|
|
|
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions());
|
|
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
|
|
|
/// <inheritdoc />
|
|
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
|
|
|
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
|
{
|
|
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
|
|
}
|
|
|
|
public override TimeSpan? GetTimeOffset()
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
|
=> new TestAuthProvider(credentials);
|
|
|
|
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync()
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
public override TimeSyncInfo GetTimeSyncInfo()
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
}
|
|
|
|
public class TestError
|
|
{
|
|
[JsonPropertyName("errorCode")]
|
|
public int ErrorCode { get; set; }
|
|
[JsonPropertyName("errorMessage")]
|
|
public string ErrorMessage { get; set; }
|
|
}
|
|
|
|
public class ParseErrorTestRestClient: TestRestClient
|
|
{
|
|
public ParseErrorTestRestClient() { }
|
|
|
|
}
|
|
}
|