Skip to content

Commit

Permalink
Client Configuration (#219)
Browse files Browse the repository at this point in the history
Added support for IOptions injection, allowing options to be read from IConfiguration
Small refactor on client options internals
Updated HttpClient to be static field to be
  • Loading branch information
JKorf authored Nov 19, 2024
1 parent 4879703 commit 7d7bc35
Show file tree
Hide file tree
Showing 17 changed files with 312 additions and 237 deletions.
26 changes: 21 additions & 5 deletions CryptoExchange.Net.UnitTests/OptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ public void TestClientUsesCorrectOptionsWithDefault()
Assert.That(authProvider1.GetSecret() == "222");
Assert.That(authProvider2.GetKey() == "123");
Assert.That(authProvider2.GetSecret() == "456");

// Cleanup static values
TestClientOptions.Default.ApiCredentials = null;
TestClientOptions.Default.Api1Options.ApiCredentials = null;
}

[Test]
Expand All @@ -121,6 +125,10 @@ public void TestClientUsesCorrectOptionsWithOverridingDefault()
Assert.That(authProvider2.GetKey() == "123");
Assert.That(authProvider2.GetSecret() == "456");
Assert.That(client.Api2.BaseAddress == "https://localhost:123");

// Cleanup static values
TestClientOptions.Default.ApiCredentials = null;
TestClientOptions.Default.Api1Options.ApiCredentials = null;
}
}

Expand All @@ -134,6 +142,14 @@ public class TestClientOptions: RestExchangeOptions<TestEnvironment, ApiCredenti
Environment = new TestEnvironment("test", "https://test.com")
};

/// <summary>
/// ctor
/// </summary>
public TestClientOptions()
{
Default?.Set(this);
}

/// <summary>
/// The default receive window for requests
/// </summary>
Expand All @@ -143,12 +159,12 @@ public class TestClientOptions: RestExchangeOptions<TestEnvironment, ApiCredenti

public RestApiOptions Api2Options { get; set; } = new RestApiOptions();

internal TestClientOptions Copy()
internal TestClientOptions Set(TestClientOptions targetOptions)
{
var options = Copy<TestClientOptions>();
options.Api1Options = Api1Options.Copy<RestApiOptions>();
options.Api2Options = Api2Options.Copy<RestApiOptions>();
return options;
targetOptions = base.Set<TestClientOptions>(targetOptions);
targetOptions.Api1Options = Api1Options.Set(targetOptions.Api1Options);
targetOptions.Api2Options = Api2Options.Set(targetOptions.Api2Options);
return targetOptions;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class TestBaseClient: BaseClient

public TestBaseClient(): base(null, "Test")
{
var options = TestClientOptions.Default.Copy();
var options = new TestClientOptions();
Initialize(options);
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
}
Expand Down
18 changes: 7 additions & 11 deletions CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Microsoft.Extensions.Logging;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Options;

namespace CryptoExchange.Net.UnitTests.TestImplementations
{
Expand All @@ -24,22 +25,17 @@ public class TestRestClient: BaseRestClient
public TestRestApi1Client Api1 { get; }
public TestRestApi2Client Api2 { get; }

public TestRestClient(Action<TestClientOptions> optionsFunc) : this(optionsFunc, null)
public TestRestClient(Action<TestClientOptions>? optionsDelegate = null)

Check warning on line 28 in CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 28 in CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
: this(null, null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
{
}

public TestRestClient(ILoggerFactory loggerFactory = null, HttpClient httpClient = null) : this((x) => { }, httpClient, loggerFactory)
public TestRestClient(HttpClient? httpClient, ILoggerFactory? loggerFactory, IOptions<TestClientOptions> options) : base(loggerFactory, "Test")

Check warning on line 33 in CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 33 in CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 33 in CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 33 in CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
{
}

public TestRestClient(Action<TestClientOptions> optionsFunc, HttpClient httpClient = null, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
{
var options = TestClientOptions.Default.Copy();
optionsFunc(options);
Initialize(options);
Initialize(options.Value);

Api1 = new TestRestApi1Client(options);
Api2 = new TestRestApi2Client(options);
Api1 = new TestRestApi1Client(options.Value);
Api2 = new TestRestApi2Client(options.Value);
}

public void SetResponse(string responseData, out IRequest requestObj)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,28 @@
using Moq;
using CryptoExchange.Net.Testing.Implementations;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Options;

namespace CryptoExchange.Net.UnitTests.TestImplementations
{
internal class TestSocketClient: BaseSocketClient
{
public TestSubSocketClient SubClient { get; }

public TestSocketClient(ILoggerFactory loggerFactory = null) : this((x) => { }, loggerFactory)
{
}

/// <summary>
/// Create a new instance of KucoinSocketClient
/// </summary>
/// <param name="optionsFunc">Configure the options to use for this client</param>
public TestSocketClient(Action<TestSocketOptions> optionsFunc) : this(optionsFunc, null)
public TestSocketClient(Action<TestSocketOptions>? optionsDelegate = null)

Check warning on line 30 in CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 30 in CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
: this(Options.Create(ApplyOptionsDelegate(optionsDelegate)), null)
{
}

public TestSocketClient(Action<TestSocketOptions> optionsFunc, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
public TestSocketClient(IOptions<TestSocketOptions> options, ILoggerFactory? loggerFactory = null) : base(loggerFactory, "Test")

Check warning on line 35 in CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 35 in CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
{
var options = TestSocketOptions.Default.Copy<TestSocketOptions>();
optionsFunc(options);
Initialize(options);
Initialize(options.Value);

SubClient = AddApiClient(new TestSubSocketClient(options, options.SubOptions));
SubClient = AddApiClient(new TestSubSocketClient(options.Value, options.Value.SubOptions));
SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object;
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com"));
}
Expand Down Expand Up @@ -70,7 +66,22 @@ public class TestSocketOptions: SocketExchangeOptions<TestEnvironment>
Environment = new TestEnvironment("Live", "https://test.test")
};

/// <summary>
/// ctor
/// </summary>
public TestSocketOptions()
{
Default?.Set(this);
}

public SocketApiOptions SubOptions { get; set; } = new SocketApiOptions();

internal TestSocketOptions Set(TestSocketOptions targetOptions)
{
targetOptions = base.Set<TestSocketOptions>(targetOptions);
targetOptions.SubOptions = SubOptions.Set(targetOptions.SubOptions);
return targetOptions;
}
}

public class TestSubSocketClient : SocketApiClient
Expand Down
29 changes: 9 additions & 20 deletions CryptoExchange.Net/Authentication/ApiCredentials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,30 @@ public class ApiCredentials
/// <summary>
/// The api key / label to authenticate requests
/// </summary>
public string Key { get; }
public string Key { get; set; }

/// <summary>
/// The api secret or private key to authenticate requests
/// </summary>
public string Secret { get; }
public string Secret { get; set; }

/// <summary>
/// Type of the credentials
/// </summary>
public ApiCredentialsType CredentialType { get; }
public ApiCredentialsType CredentialType { get; set; }

/// <summary>
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
/// <param name="key">The api key / label used for identification</param>
/// <param name="secret">The api secret or private key used for signing</param>
public ApiCredentials(string key, string secret) : this(key, secret, ApiCredentialsType.Hmac)
{
}

/// <summary>
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
/// <param name="key">The api key / label used for identification</param>
/// <param name="secret">The api secret or private key used for signing</param>
/// <param name="credentialsType">The type of credentials</param>
public ApiCredentials(string key, string secret, ApiCredentialsType credentialsType)
/// <param name="credentialType">The type of credentials</param>
public ApiCredentials(string key, string secret, ApiCredentialsType credentialType = ApiCredentialsType.Hmac)
{
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
throw new ArgumentException("Key and secret can't be null/empty");

CredentialType = credentialsType;
CredentialType = credentialType;
Key = key;
Secret = secret;
}
Expand All @@ -65,7 +56,7 @@ public virtual ApiCredentials Copy()
/// <param name="inputStream">The stream containing the json data</param>
/// <param name="identifierKey">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'.</param>
/// <param name="identifierSecret">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'.</param>
public ApiCredentials(Stream inputStream, string? identifierKey = null, string? identifierSecret = null)
public static ApiCredentials FromStream(Stream inputStream, string? identifierKey = null, string? identifierSecret = null)
{
var accessor = new SystemTextJsonStreamMessageAccessor();
if (!accessor.Read(inputStream, false).Result)
Expand All @@ -75,11 +66,9 @@ public ApiCredentials(Stream inputStream, string? identifierKey = null, string?
var secret = accessor.GetValue<string>(MessagePath.Get().Property(identifierSecret ?? "apiSecret"));
if (key == null || secret == null)
throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");

Key = key;
Secret = secret;


inputStream.Seek(0, SeekOrigin.Begin);
return new ApiCredentials(key, secret);
}
}
}
4 changes: 2 additions & 2 deletions CryptoExchange.Net/Authentication/AuthenticationProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ public abstract class AuthenticationProvider
/// <summary>
/// Get the API key of the current credentials
/// </summary>
public string ApiKey => _credentials.Key;
public string ApiKey => _credentials.Key!;

/// <summary>
/// ctor
/// </summary>
/// <param name="credentials"></param>
protected AuthenticationProvider(ApiCredentials credentials)
{
if (credentials.Secret == null)
if (credentials.Key == null || credentials.Secret == null)
throw new ArgumentException("ApiKey/Secret needed");

_credentials = credentials;
Expand Down
10 changes: 10 additions & 0 deletions CryptoExchange.Net/Clients/BaseClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,16 @@ protected T AddApiClient<T>(T apiClient) where T : BaseApiClient
return apiClient;
}

/// <summary>
/// Apply the options delegate to a new options instance
/// </summary>
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T: new()
{
var opts = new T();
del?.Invoke(opts);
return opts;
}

/// <summary>
/// Dispose
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions CryptoExchange.Net/CryptoExchange.Net.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,6 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
</ItemGroup>
</Project>
19 changes: 5 additions & 14 deletions CryptoExchange.Net/Objects/ApiProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,21 @@ public class ApiProxy
/// <summary>
/// The host address of the proxy
/// </summary>
public string Host { get; }
public string Host { get; set; }
/// <summary>
/// The port of the proxy
/// </summary>
public int Port { get; }
public int Port { get; set; }

/// <summary>
/// The login of the proxy
/// </summary>
public string? Login { get; }
public string? Login { get; set; }

/// <summary>
/// The password of the proxy
/// </summary>
public string? Password { get; }

/// <summary>
/// Create new settings for a proxy
/// </summary>
/// <param name="host">The proxy hostname/ip</param>
/// <param name="port">The proxy port</param>
public ApiProxy(string host, int port): this(host, port, null, null)
{
}
public string? Password { get; set; }

/// <summary>
/// Create new settings for a proxy
Expand All @@ -40,7 +31,7 @@ public ApiProxy(string host, int port): this(host, port, null, null)
/// <param name="port">The proxy port</param>
/// <param name="login">The proxy login</param>
/// <param name="password">The proxy password</param>
public ApiProxy(string host, int port, string? login, string? password)
public ApiProxy(string host, int port, string? login = null, string? password = null)
{
Host = host;
Port = port;
Expand Down
18 changes: 7 additions & 11 deletions CryptoExchange.Net/Objects/Options/RestApiOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,15 @@ public class RestApiOptions : ApiOptions
public TimeSpan? TimestampRecalculationInterval { get; set; }

/// <summary>
/// Create a copy of this options
/// Set the values of this options on the target options
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public virtual T Copy<T>() where T : RestApiOptions, new()
public T Set<T>(T item) where T : RestApiOptions, new()
{
return new T
{
ApiCredentials = ApiCredentials?.Copy(),
OutputOriginalData = OutputOriginalData,
AutoTimestamp = AutoTimestamp,
TimestampRecalculationInterval = TimestampRecalculationInterval
};
item.ApiCredentials = ApiCredentials?.Copy();
item.OutputOriginalData = OutputOriginalData;
item.AutoTimestamp = AutoTimestamp;
item.TimestampRecalculationInterval = TimestampRecalculationInterval;
return item;
}
}

Expand Down
Loading

0 comments on commit 7d7bc35

Please sign in to comment.