From 6215931269b6ed305d8f2d096ad68de2785dbc75 Mon Sep 17 00:00:00 2001 From: Oleksii Zuiev Date: Fri, 13 Dec 2024 02:29:26 +0100 Subject: [PATCH] Add possibility to add AWS services to DI with a key (#3570) * Include newer versions of dependencies if target framework is .NET 8 to utilize newer framework features * Add extensions to add AWS services with a key, to enable multiple registrations of the same type --- .../AWSSDK.Extensions.NETCore.Setup.csproj | 10 +- .../ServiceCollectionExtensions.cs | 73 +++++++++ .../DependencyInjectionTests.cs | 151 ++++++++++++++++++ .../NETCore.SetupTests.csproj | 11 +- 4 files changed, 243 insertions(+), 2 deletions(-) diff --git a/extensions/src/AWSSDK.Extensions.NETCore.Setup/AWSSDK.Extensions.NETCore.Setup.csproj b/extensions/src/AWSSDK.Extensions.NETCore.Setup/AWSSDK.Extensions.NETCore.Setup.csproj index a690301ebf28..80f0c4946124 100644 --- a/extensions/src/AWSSDK.Extensions.NETCore.Setup/AWSSDK.Extensions.NETCore.Setup.csproj +++ b/extensions/src/AWSSDK.Extensions.NETCore.Setup/AWSSDK.Extensions.NETCore.Setup.csproj @@ -35,9 +35,17 @@ - + + + + + + + + + diff --git a/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs b/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs index e0ec095025bd..c0cf329b5104 100644 --- a/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs +++ b/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs @@ -162,5 +162,78 @@ public static IServiceCollection TryAddAWSService(this IServiceCollection col collection.TryAdd(descriptor); return collection; } + +#if NET8_0_OR_GREATER + + /// + /// Adds the Amazon service client to the dependency injection framework with a key. The Amazon service client is not + /// created until it is requested. If the ServiceLifetime property is set to Singleton, the default, then the same + /// instance will be reused for the lifetime of the process and the object should not be disposed. + /// + /// The AWS service interface, like IAmazonS3 + /// + /// The key with which the service will be added in the dependency injection framework. + /// The lifetime of the service client created. The default is Singleton. + /// Returns back the IServiceCollection to continue the fluent system of IServiceCollection. + public static IServiceCollection AddKeyedAWSService(this IServiceCollection collection, object serviceKey, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService + { + return AddKeyedAWSService(collection, serviceKey, null, lifetime); + } + + /// + /// Adds the Amazon service client to the dependency injection framework with a key. The Amazon service client is not + /// created until it is requested. If the ServiceLifetime property is set to Singleton, the default, then the same + /// instance will be reused for the lifetime of the process and the object should not be disposed. + /// + /// The AWS service interface, like IAmazonS3 + /// + /// The key with which the service will be added in the dependency injection framework. + /// The AWS options used to create the service client overriding the default AWS options added using AddDefaultAWSOptions. + /// The lifetime of the service client created. The default is Singleton. + /// Returns back the IServiceCollection to continue the fluent system of IServiceCollection. + public static IServiceCollection AddKeyedAWSService(this IServiceCollection collection, object serviceKey, AWSOptions options, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService + { + object Factory(IServiceProvider sp, object key) => new ClientFactory(options).CreateServiceClient(sp); + + var descriptor = new ServiceDescriptor(typeof(T), serviceKey, Factory, lifetime); + collection.Add(descriptor); + return collection; + } + + /// + /// Adds the Amazon service client to the dependency injection framework with a key if the service type hasn't already been registered with the same key. + /// The Amazon service client is not created until it is requested. If the ServiceLifetime property is set to Singleton, the default, then the same + /// instance will be reused for the lifetime of the process and the object should not be disposed. + /// + /// The AWS service interface, like IAmazonS3 + /// + /// The key with which the service will be added in the dependency injection framework. + /// The lifetime of the service client created. The default is Singleton. + /// Returns back the IServiceCollection to continue the fluent system of IServiceCollection. + public static IServiceCollection TryAddKeyedAWSService(this IServiceCollection collection, object serviceKey, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService + { + return TryAddKeyedAWSService(collection, serviceKey, null, lifetime); + } + + /// + /// Adds the Amazon service client to the dependency injection framework with a key if the service type hasn't already been registered with the same key. + /// The Amazon service client is not created until it is requested. If the ServiceLifetime property is set to Singleton, the default, then the same + /// instance will be reused for the lifetime of the process and the object should not be disposed. + /// + /// The AWS service interface, like IAmazonS3 + /// + /// The key with which the service will be added in the dependency injection framework. + /// The AWS options used to create the service client overriding the default AWS options added using AddDefaultAWSOptions. + /// The lifetime of the service client created. The default is Singleton. + /// Returns back the IServiceCollection to continue the fluent system of IServiceCollection. + public static IServiceCollection TryAddKeyedAWSService(this IServiceCollection collection, object serviceKey, AWSOptions options, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService + { + object Factory(IServiceProvider sp, object key) => new ClientFactory(options).CreateServiceClient(sp); + + var descriptor = new ServiceDescriptor(typeof(T), serviceKey, Factory, lifetime); + collection.TryAdd(descriptor); + return collection; + } +#endif } } diff --git a/extensions/test/NETCore.SetupTests/DependencyInjectionTests.cs b/extensions/test/NETCore.SetupTests/DependencyInjectionTests.cs index bfd897d657fb..4aba7d16a07e 100644 --- a/extensions/test/NETCore.SetupTests/DependencyInjectionTests.cs +++ b/extensions/test/NETCore.SetupTests/DependencyInjectionTests.cs @@ -190,6 +190,157 @@ public void InjectS3ClientWithFactoryBuiltConfig() Assert.NotNull(controller.S3Client); Assert.Equal(expectRegion, controller.S3Client.Config.RegionEndpoint); } + +#if NET8_0_OR_GREATER + + [Fact] + public void InjectKeyedS3ClientWithDefaultConfig() + { + var builder = new ConfigurationBuilder(); + builder.AddJsonFile("./TestFiles/GetClientConfigSettingsTest.json"); + + IConfiguration config = builder.Build(); + + ServiceCollection services = new ServiceCollection(); + services.AddDefaultAWSOptions(config.GetAWSOptions()); + services.AddKeyedAWSService(TestControllerKeyed.Key); + + var serviceProvider = services.BuildServiceProvider(); + + var controller = ActivatorUtilities.CreateInstance(serviceProvider); + Assert.NotNull(controller.S3Client); + Assert.Equal(RegionEndpoint.USWest2, controller.S3Client.Config.RegionEndpoint); + } + + [Fact] + public void InjectKeyedS3ClientWithoutDefaultConfig() + { + ServiceCollection services = new ServiceCollection(); + services.AddKeyedAWSService(TestControllerKeyed.Key, new AWSOptions {Region = RegionEndpoint.USEast1 }); + + var serviceProvider = services.BuildServiceProvider(); + + var controller = ActivatorUtilities.CreateInstance(serviceProvider); + Assert.NotNull(controller.S3Client); + Assert.Equal(RegionEndpoint.USEast1, controller.S3Client.Config.RegionEndpoint); + } + + [Fact] + public void InjectKeyedS3ClientWithOverridingConfig() + { + var builder = new ConfigurationBuilder(); + builder.AddJsonFile("./TestFiles/GetClientConfigSettingsTest.json"); + + IConfiguration config = builder.Build(); + + ServiceCollection services = new ServiceCollection(); + services.AddDefaultAWSOptions(config.GetAWSOptions()); + services.AddKeyedAWSService(TestControllerKeyed.Key, new AWSOptions {Region = RegionEndpoint.EUCentral1 }); + + var serviceProvider = services.BuildServiceProvider(); + + var controller = ActivatorUtilities.CreateInstance(serviceProvider); + Assert.NotNull(controller.S3Client); + Assert.Equal(RegionEndpoint.EUCentral1, controller.S3Client.Config.RegionEndpoint); + } + + [Fact] + public void TryAddKeyedCanRegisterWithDefaultConfig() + { + var builder = new ConfigurationBuilder(); + builder.AddJsonFile("./TestFiles/GetClientConfigSettingsTest.json"); + + IConfiguration config = builder.Build(); + + ServiceCollection services = new ServiceCollection(); + services.AddDefaultAWSOptions(config.GetAWSOptions()); + services.TryAddKeyedAWSService(TestControllerKeyed.Key); + + var serviceProvider = services.BuildServiceProvider(); + + var controller = ActivatorUtilities.CreateInstance(serviceProvider); + Assert.NotNull(controller.S3Client); + Assert.Equal(RegionEndpoint.USWest2, controller.S3Client.Config.RegionEndpoint); + } + + [Fact] + public void TryAddKeyedCanRegisterWithoutDefaultConfig() + { + ServiceCollection services = new ServiceCollection(); + services.AddKeyedAWSService(TestControllerKeyed.Key, new AWSOptions {Region = RegionEndpoint.USEast1 }); + + var serviceProvider = services.BuildServiceProvider(); + + var controller = ActivatorUtilities.CreateInstance(serviceProvider); + Assert.NotNull(controller.S3Client); + Assert.Equal(RegionEndpoint.USEast1, controller.S3Client.Config.RegionEndpoint); + } + + [Fact] + public void TryAddKeyedServiceDontOverrideWhenAlreadySetup() + { + var builder = new ConfigurationBuilder(); + builder.AddJsonFile("./TestFiles/GetClientConfigSettingsTest.json"); + + IConfiguration config = builder.Build(); + + ServiceCollection services = new ServiceCollection(); + + services.AddDefaultAWSOptions(config.GetAWSOptions()); + services.AddKeyedAWSService(TestControllerKeyed.Key, new AWSOptions { Region = RegionEndpoint.EUWest1 }); + services.TryAddKeyedAWSService(TestControllerKeyed.Key); + services.TryAddKeyedAWSService(TestControllerKeyed.Key, new AWSOptions { Region = RegionEndpoint.EUCentral1 }); + + var serviceProvider = services.BuildServiceProvider(); + + var controller = ActivatorUtilities.CreateInstance(serviceProvider); + Assert.Equal(RegionEndpoint.EUWest1, controller.S3Client.Config.RegionEndpoint); + } + + [Fact] + public void InjectMultipleS3Clients() + { + ServiceCollection services = new ServiceCollection(); + services.AddKeyedAWSService(TestControllerMultiKeyed.Key1, new AWSOptions { Region = RegionEndpoint.USEast1 }); + services.AddKeyedAWSService(TestControllerMultiKeyed.Key2, new AWSOptions { Region = RegionEndpoint.USWest2 }); + + var serviceProvider = services.BuildServiceProvider(); + + var controller = ActivatorUtilities.CreateInstance(serviceProvider); + Assert.NotNull(controller.S3Client1); + Assert.NotNull(controller.S3Client2); + Assert.Equal(RegionEndpoint.USEast1, controller.S3Client1.Config.RegionEndpoint); + Assert.Equal(RegionEndpoint.USWest2, controller.S3Client2.Config.RegionEndpoint); + } + + public class TestControllerKeyed + { + public const string Key = "key"; + + public IAmazonS3 S3Client { get; private set; } + public TestControllerKeyed([FromKeyedServices(Key)] IAmazonS3 s3Client) + { + S3Client = s3Client; + } + } + + public class TestControllerMultiKeyed + { + public const string Key1 = "key1"; + public const string Key2 = "key2"; + + public IAmazonS3 S3Client1 { get; private set; } + public IAmazonS3 S3Client2 { get; private set; } + public TestControllerMultiKeyed( + [FromKeyedServices(Key1)] IAmazonS3 s3Client1, + [FromKeyedServices(Key2)] IAmazonS3 s3Client2) + { + S3Client1 = s3Client1; + S3Client2 = s3Client2; + } + + } +#endif public class TestController { diff --git a/extensions/test/NETCore.SetupTests/NETCore.SetupTests.csproj b/extensions/test/NETCore.SetupTests/NETCore.SetupTests.csproj index abf26cb7ca67..df2a9cc38784 100644 --- a/extensions/test/NETCore.SetupTests/NETCore.SetupTests.csproj +++ b/extensions/test/NETCore.SetupTests/NETCore.SetupTests.csproj @@ -25,10 +25,19 @@ + + + + - + + + + + +