1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-10-14 15:11:44 +00:00
This commit is contained in:
Jkorf
2026-09-15 10:04:46 +02:00
parent c59bb0f625
commit 45a42fb028
7 changed files with 178 additions and 149 deletions
@@ -63,7 +63,6 @@ namespace CryptoExchange.Net.UnitTests
var restApi = new TestRestSharedApi();
var socketApi = new TestSocketSharedApi();
var client = new TestDiSharedApiClient(
SharedTransport.Socket,
restApi,
socketApi);
@@ -82,26 +81,48 @@ namespace CryptoExchange.Net.UnitTests
}
[Test]
public void RegisterSharedApiClientCapabilities_ShouldResolvePreferredTransport()
public void RegisterSharedApiClient_ShouldResolvePreferredTransport()
{
var restApi = new TestRestSharedApi();
var socketApi = new TestSocketSharedApi();
var services = new ServiceCollection();
services.RegisterSharedApi<ITestRestSharedApi>(_ => restApi);
services.RegisterSharedApi<ITestSocketSharedApi>(_ => socketApi);
services.AddTransient<ITestSharedApiClient>(_ =>
new TestDiSharedApiClient(SharedTransport.Socket, restApi, socketApi));
services.RegisterSharedApiClientCapabilities<ITestSharedApiClient>();
services.AddSingleton(restApi);
services.AddSingleton(socketApi);
services.RegisterSharedApiClient<
ITestSharedApiClient,
TestDiSharedApiClient>(sharedApis => sharedApis
.Add(client => client.Rest)
.Add(client => client.Socket));
using var provider = services.BuildServiceProvider();
Assert.Multiple(() =>
{
Assert.That(provider.GetRequiredService<ITestCapability>(), Is.SameAs(socketApi));
Assert.That(provider.GetServices<ITestCapability>().ToArray(), Is.EqualTo(new[] { socketApi }));
Assert.That(provider.GetRequiredService<ITestRestCapability>(), Is.SameAs(restApi));
Assert.That(provider.GetRequiredService<ITestSocketCapability>(), Is.SameAs(socketApi));
Assert.That(
provider.GetRequiredService<ITestSharedApiClient>(),
Is.Not.Null);
Assert.That(
provider.GetRequiredService<ISharedApiClientBase>(),
Is.InstanceOf<TestDiSharedApiClient>());
Assert.That(
provider.GetRequiredService<ITestCapability>(),
Is.SameAs(socketApi));
Assert.That(
provider.GetServices<ITestCapability>().ToArray(),
Is.EqualTo(new[] { socketApi }));
Assert.That(
provider.GetRequiredService<ITestRestCapability>(),
Is.SameAs(restApi));
Assert.That(
provider.GetRequiredService<ITestSocketCapability>(),
Is.SameAs(socketApi));
});
}
@@ -292,10 +313,9 @@ namespace CryptoExchange.Net.UnitTests
public ITestSocketSharedApi Socket { get; }
public TestDiSharedApiClient(
SharedTransport preferredTransport,
ITestRestSharedApi rest,
ITestSocketSharedApi socket)
: base(preferredTransport, rest, socket)
TestRestSharedApi rest,
TestSocketSharedApi socket)
: base(SharedTransport.Socket, rest, socket)
{
Rest = rest;
Socket = socket;
-114
View File
@@ -474,120 +474,6 @@ namespace CryptoExchange.Net
return services;
}
/// <summary>
/// Registers a strict Shared API aggregate and all capability interfaces implemented by it.
/// </summary>
public static IServiceCollection RegisterSharedApi<
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)]
#endif
TSharedApi>(
this IServiceCollection services,
Func<IServiceProvider, TSharedApi> factory)
where TSharedApi : class, ISharedApi
{
services.AddTransient<TSharedApi>(factory);
var allInterfaces = typeof(TSharedApi).GetInterfaces();
var transportCapabilityInterfaces = allInterfaces
.Where(x =>
typeof(ISharedApiCapability).IsAssignableFrom(x)
&& (typeof(ISharedRest).IsAssignableFrom(x)
|| typeof(ISharedSocket).IsAssignableFrom(x))
&& x != typeof(ISharedRest)
&& x != typeof(ISharedSocket)
&& x != typeof(ISharedSubscription))
.Distinct()
.ToArray();
var baseCapabilityInterfaces = allInterfaces
.Where(x =>
x != typeof(ISharedApiCapability)
&& typeof(ISharedApiCapability).IsAssignableFrom(x)
&& !typeof(ISharedRest).IsAssignableFrom(x)
&& !typeof(ISharedSocket).IsAssignableFrom(x))
.Distinct()
.ToArray();
services.AddSingleton(new SharedApiRegistrationInfo(
typeof(TSharedApi),
baseCapabilityInterfaces));
foreach (var capabilityInterface in transportCapabilityInterfaces)
{
services.AddTransient(
capabilityInterface,
serviceProvider =>
serviceProvider.GetRequiredService<TSharedApi>());
}
return services;
}
/// <summary>
/// Registers transport-agnostic Shared API capabilities through the
/// exchange-specific Shared API aggregate client.
/// </summary>
public static IServiceCollection RegisterSharedApiClientCapabilities<
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicConstructors)]
#endif
TSharedApiClient>(
this IServiceCollection services)
where TSharedApiClient : class, ISharedApiClientBase
{
services.AddTransient<ISharedApiClientBase>(serviceProvider =>
serviceProvider.GetRequiredService<TSharedApiClient>());
var sharedApiTypes = new HashSet<Type>(
typeof(TSharedApiClient)
.GetProperties()
.Where(property =>
typeof(ISharedApi).IsAssignableFrom(
property.PropertyType))
.Select(property => property.PropertyType));
var capabilityInterfaces = services
.Where(descriptor =>
descriptor.ImplementationInstance
is SharedApiRegistrationInfo)
.Select(descriptor =>
(SharedApiRegistrationInfo)descriptor.ImplementationInstance!)
.Where(registration =>
sharedApiTypes.Contains(registration.SharedApiType))
.SelectMany(registration =>
registration.BaseCapabilityTypes)
.Distinct()
.ToArray();
foreach (var capabilityInterface in capabilityInterfaces)
{
services.AddTransient(
capabilityInterface,
serviceProvider =>
{
var sharedApiClient = serviceProvider
.GetRequiredService<TSharedApiClient>();
if (sharedApiClient is not ISharedApiClientResolver resolver)
{
throw new InvalidOperationException(
$"{typeof(TSharedApiClient).Name} should derive from " +
$"{nameof(SharedApiClientBase)}");
}
return resolver.GetCapability(capabilityInterface)
?? throw new InvalidOperationException(
$"No implementation of " +
$"{capabilityInterface.Name} is available");
});
}
return services;
}
/// <summary>
/// Convert a hex encoded string to byte array
/// </summary>
@@ -7,7 +7,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// References for a capability available through REST.
/// </summary>
public sealed class SharedRestCapability<TCapability, TRest>
public class SharedRestCapability<TCapability, TRest>
: SharedCapabilityReference<TCapability>
where TCapability : ISharedApiCapability
where TRest : TCapability, ISharedRest
@@ -7,7 +7,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// References for a capability available through REST and WebSocket.
/// </summary>
public sealed class SharedRestSocketCapability<TCapability, TRest, TSocket>
public class SharedRestSocketCapability<TCapability, TRest, TSocket>
: SharedCapabilityReference<TCapability>
where TCapability : ISharedApiCapability
where TRest : TCapability, ISharedRest
@@ -0,0 +1,110 @@
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Shared API dependency injection registration builder
/// </summary>
/// <typeparam name="TSharedApiClient"></typeparam>
public class SharedApiClientRegistrationBuilder<TSharedApiClient>
where TSharedApiClient : class, ISharedApiClientBase
{
private readonly IServiceCollection _services;
private readonly HashSet<Type> _baseCapabilityTypes = new();
internal SharedApiClientRegistrationBuilder(
IServiceCollection services)
{
_services = services;
}
/// <summary>
/// Adds a shared API
/// </summary>
public SharedApiClientRegistrationBuilder<TSharedApiClient> Add<
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.Interfaces)]
#endif
TSharedApi>(
Func<TSharedApiClient, TSharedApi> selector)
where TSharedApi : class, ISharedApi
{
if (selector is null)
throw new ArgumentNullException(nameof(selector));
_services.AddTransient<TSharedApi>(
serviceProvider =>
{
var client = serviceProvider
.GetRequiredService<TSharedApiClient>();
return selector(client);
});
var interfaces = typeof(TSharedApi).GetInterfaces();
var transportCapabilities = interfaces
.Where(type =>
typeof(ISharedApiCapability).IsAssignableFrom(type)
&& (typeof(ISharedRest).IsAssignableFrom(type)
|| typeof(ISharedSocket).IsAssignableFrom(type))
&& type != typeof(ISharedRest)
&& type != typeof(ISharedSocket)
&& type != typeof(ISharedSubscription))
.Distinct();
foreach (var capabilityType in transportCapabilities)
{
_services.AddTransient(
capabilityType,
serviceProvider =>
{
var client = serviceProvider
.GetRequiredService<TSharedApiClient>();
return selector(client);
});
}
var baseCapabilities = interfaces
.Where(type =>
type != typeof(ISharedApiCapability)
&& typeof(ISharedApiCapability).IsAssignableFrom(type)
&& !typeof(ISharedRest).IsAssignableFrom(type)
&& !typeof(ISharedSocket).IsAssignableFrom(type));
foreach (var capabilityType in baseCapabilities)
_baseCapabilityTypes.Add(capabilityType);
return this;
}
internal void RegisterTransportAgnosticCapabilities()
{
foreach (var capabilityType in _baseCapabilityTypes)
{
_services.AddTransient(
capabilityType,
serviceProvider =>
{
var client = serviceProvider
.GetRequiredService<TSharedApiClient>();
var resolver = (ISharedApiClientResolver)client;
return resolver.GetCapability(capabilityType)
?? throw new InvalidOperationException(
$"No implementation of " +
$"{capabilityType.Name} is available on " +
$"{typeof(TSharedApiClient).Name}");
});
}
}
}
}
@@ -1,18 +0,0 @@
using System;
namespace CryptoExchange.Net.SharedApis
{
internal sealed class SharedApiRegistrationInfo
{
public Type SharedApiType { get; }
public Type[] BaseCapabilityTypes { get; }
public SharedApiRegistrationInfo(
Type sharedApiType,
Type[] baseCapabilityTypes)
{
SharedApiType = sharedApiType;
BaseCapabilityTypes = baseCapabilityTypes;
}
}
}
@@ -1,5 +1,8 @@
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace CryptoExchange.Net.SharedApis
@@ -45,5 +48,33 @@ namespace CryptoExchange.Net.SharedApis
resultData = resultData.Where(x => x.QuoteAssetSubType == request.QuoteAssetSubType);
return resultData.ToArray();
}
/// <summary>
/// Register Shared API client in DI container
/// </summary>
public static IServiceCollection RegisterSharedApiClient<
TSharedApiClient,
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicConstructors)]
#endif
TImplementation
>(this IServiceCollection services, Action<SharedApiClientRegistrationBuilder<TSharedApiClient>> configure)
where TImplementation : class, TSharedApiClient
where TSharedApiClient : class, ISharedApiClientBase
{
services.AddTransient<TSharedApiClient, TImplementation>();
services.AddTransient<ISharedApiClientBase>(
serviceProvider =>
serviceProvider.GetRequiredService<TSharedApiClient>());
var builder =
new SharedApiClientRegistrationBuilder<TSharedApiClient>(services);
configure(builder);
builder.RegisterTransportAgnosticCapabilities();
return services;
}
}
}