diff --git a/CryptoExchange.Net.UnitTests/SharedApiClientTests.cs b/CryptoExchange.Net.UnitTests/SharedApiClientTests.cs index e1b48287..c3635699 100644 --- a/CryptoExchange.Net.UnitTests/SharedApiClientTests.cs +++ b/CryptoExchange.Net.UnitTests/SharedApiClientTests.cs @@ -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(_ => restApi); - services.RegisterSharedApi(_ => socketApi); - services.AddTransient(_ => - new TestDiSharedApiClient(SharedTransport.Socket, restApi, socketApi)); - services.RegisterSharedApiClientCapabilities(); + 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(), Is.SameAs(socketApi)); - Assert.That(provider.GetServices().ToArray(), Is.EqualTo(new[] { socketApi })); - Assert.That(provider.GetRequiredService(), Is.SameAs(restApi)); - Assert.That(provider.GetRequiredService(), Is.SameAs(socketApi)); + Assert.That( + provider.GetRequiredService(), + Is.Not.Null); + + Assert.That( + provider.GetRequiredService(), + Is.InstanceOf()); + + Assert.That( + provider.GetRequiredService(), + Is.SameAs(socketApi)); + + Assert.That( + provider.GetServices().ToArray(), + Is.EqualTo(new[] { socketApi })); + + Assert.That( + provider.GetRequiredService(), + Is.SameAs(restApi)); + + Assert.That( + provider.GetRequiredService(), + 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; diff --git a/CryptoExchange.Net/ExtensionMethods.cs b/CryptoExchange.Net/ExtensionMethods.cs index 8f2ccf1f..6a3ee118 100644 --- a/CryptoExchange.Net/ExtensionMethods.cs +++ b/CryptoExchange.Net/ExtensionMethods.cs @@ -474,120 +474,6 @@ namespace CryptoExchange.Net return services; } - /// - /// Registers a strict Shared API aggregate and all capability interfaces implemented by it. - /// - public static IServiceCollection RegisterSharedApi< -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] -#endif - TSharedApi>( - this IServiceCollection services, - Func factory) - where TSharedApi : class, ISharedApi - { - services.AddTransient(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()); - } - - return services; - } - - /// - /// Registers transport-agnostic Shared API capabilities through the - /// exchange-specific Shared API aggregate client. - /// - 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(serviceProvider => - serviceProvider.GetRequiredService()); - - var sharedApiTypes = new HashSet( - 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(); - - 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; - } - /// /// Convert a hex encoded string to byte array /// diff --git a/CryptoExchange.Net/SharedApis/CapabilityReferences/SharedRestCapability.cs b/CryptoExchange.Net/SharedApis/CapabilityReferences/SharedRestCapability.cs index 3d20275b..6a269d04 100644 --- a/CryptoExchange.Net/SharedApis/CapabilityReferences/SharedRestCapability.cs +++ b/CryptoExchange.Net/SharedApis/CapabilityReferences/SharedRestCapability.cs @@ -7,7 +7,7 @@ namespace CryptoExchange.Net.SharedApis /// /// References for a capability available through REST. /// - public sealed class SharedRestCapability + public class SharedRestCapability : SharedCapabilityReference where TCapability : ISharedApiCapability where TRest : TCapability, ISharedRest diff --git a/CryptoExchange.Net/SharedApis/CapabilityReferences/SharedRestSocketCapability.cs b/CryptoExchange.Net/SharedApis/CapabilityReferences/SharedRestSocketCapability.cs index 7375771c..c50101fe 100644 --- a/CryptoExchange.Net/SharedApis/CapabilityReferences/SharedRestSocketCapability.cs +++ b/CryptoExchange.Net/SharedApis/CapabilityReferences/SharedRestSocketCapability.cs @@ -7,7 +7,7 @@ namespace CryptoExchange.Net.SharedApis /// /// References for a capability available through REST and WebSocket. /// - public sealed class SharedRestSocketCapability + public class SharedRestSocketCapability : SharedCapabilityReference where TCapability : ISharedApiCapability where TRest : TCapability, ISharedRest diff --git a/CryptoExchange.Net/SharedApis/SharedApiClientRegistrationBuilder.cs b/CryptoExchange.Net/SharedApis/SharedApiClientRegistrationBuilder.cs new file mode 100644 index 00000000..5d312d2e --- /dev/null +++ b/CryptoExchange.Net/SharedApis/SharedApiClientRegistrationBuilder.cs @@ -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 +{ + /// + /// Shared API dependency injection registration builder + /// + /// + public class SharedApiClientRegistrationBuilder + where TSharedApiClient : class, ISharedApiClientBase + { + private readonly IServiceCollection _services; + private readonly HashSet _baseCapabilityTypes = new(); + + internal SharedApiClientRegistrationBuilder( + IServiceCollection services) + { + _services = services; + } + + /// + /// Adds a shared API + /// + public SharedApiClientRegistrationBuilder Add< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers( + DynamicallyAccessedMemberTypes.Interfaces)] +#endif + TSharedApi>( + Func selector) + where TSharedApi : class, ISharedApi + { + if (selector is null) + throw new ArgumentNullException(nameof(selector)); + + _services.AddTransient( + serviceProvider => + { + var client = serviceProvider + .GetRequiredService(); + + 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(); + + 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(); + + var resolver = (ISharedApiClientResolver)client; + + return resolver.GetCapability(capabilityType) + ?? throw new InvalidOperationException( + $"No implementation of " + + $"{capabilityType.Name} is available on " + + $"{typeof(TSharedApiClient).Name}"); + }); + } + } + } +} diff --git a/CryptoExchange.Net/SharedApis/SharedApiRegistrationInfo.cs b/CryptoExchange.Net/SharedApis/SharedApiRegistrationInfo.cs deleted file mode 100644 index 83a2a78d..00000000 --- a/CryptoExchange.Net/SharedApis/SharedApiRegistrationInfo.cs +++ /dev/null @@ -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; - } - } -} diff --git a/CryptoExchange.Net/SharedApis/SharedUtils.cs b/CryptoExchange.Net/SharedApis/SharedUtils.cs index dce2897c..40ce34a9 100644 --- a/CryptoExchange.Net/SharedApis/SharedUtils.cs +++ b/CryptoExchange.Net/SharedApis/SharedUtils.cs @@ -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(); } + + /// + /// Register Shared API client in DI container + /// + public static IServiceCollection RegisterSharedApiClient< + TSharedApiClient, +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers( + DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + TImplementation + >(this IServiceCollection services, Action> configure) + where TImplementation : class, TSharedApiClient + where TSharedApiClient : class, ISharedApiClientBase + { + services.AddTransient(); + services.AddTransient( + serviceProvider => + serviceProvider.GetRequiredService()); + + var builder = + new SharedApiClientRegistrationBuilder(services); + + configure(builder); + builder.RegisterTransportAgnosticCapabilities(); + + return services; + } } }