From d0cf35364ff627eeb3b6ecb6a6ab65719008d936 Mon Sep 17 00:00:00 2001 From: Chase Miller Date: Thu, 20 Mar 2025 16:21:04 -0400 Subject: [PATCH 1/5] Introduce IAWSCredentialsFactory and related extension methods --- .../AWSOptions.cs | 3 +- .../ClientFactory.cs | 79 ++----- .../DefaultAWSCredentialsFactory.cs | 69 ++++++ .../IAWSCredentialsFactory.cs | 23 ++ .../ServiceCollectionExtensions.cs | 207 ++++++++++++++++-- 5 files changed, 299 insertions(+), 82 deletions(-) create mode 100644 extensions/src/AWSSDK.Extensions.NETCore.Setup/DefaultAWSCredentialsFactory.cs create mode 100644 extensions/src/AWSSDK.Extensions.NETCore.Setup/IAWSCredentialsFactory.cs diff --git a/extensions/src/AWSSDK.Extensions.NETCore.Setup/AWSOptions.cs b/extensions/src/AWSSDK.Extensions.NETCore.Setup/AWSOptions.cs index da72c7925b1b..8613fb58a2d4 100644 --- a/extensions/src/AWSSDK.Extensions.NETCore.Setup/AWSOptions.cs +++ b/extensions/src/AWSSDK.Extensions.NETCore.Setup/AWSOptions.cs @@ -21,6 +21,7 @@ using Amazon.Runtime; using Amazon.Extensions.NETCore.Setup; +using AWSSDK.Extensions.NETCore.Setup; namespace Amazon.Extensions.NETCore.Setup { @@ -110,7 +111,7 @@ internal set /// The service client that implements the service interface. public T CreateServiceClient() where T : IAmazonService { - return (T)ClientFactory.CreateServiceClient(null, typeof(T), this); + return (T)ClientFactory.CreateServiceClient(null, typeof(T), this, new DefaultAWSCredentialsFactory(this)); } /// diff --git a/extensions/src/AWSSDK.Extensions.NETCore.Setup/ClientFactory.cs b/extensions/src/AWSSDK.Extensions.NETCore.Setup/ClientFactory.cs index bfb386059098..2c903b43e9b3 100644 --- a/extensions/src/AWSSDK.Extensions.NETCore.Setup/ClientFactory.cs +++ b/extensions/src/AWSSDK.Extensions.NETCore.Setup/ClientFactory.cs @@ -16,7 +16,7 @@ using System.Reflection; using Amazon.Runtime; using Amazon.Runtime.CredentialManagement; - +using AWSSDK.Extensions.NETCore.Setup; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -34,17 +34,23 @@ internal class ClientFactory private static readonly Type[] EMPTY_TYPES = Array.Empty(); private static readonly object[] EMPTY_PARAMETERS = Array.Empty(); - private Type _serviceInterfaceType; - private AWSOptions _awsOptions; + private readonly Type _serviceInterfaceType; + private readonly AWSOptions _awsOptions; + private readonly IAWSCredentialsFactory _credentialsFactory; + private readonly ILoggerFactory _loggerFactory; + private readonly IConfiguration _configuration; /// /// Constructs an instance of the ClientFactory /// /// The type object for the Amazon service client interface, for example IAmazonS3. - internal ClientFactory(Type type, AWSOptions awsOptions) + internal ClientFactory(Type type, AWSOptions awsOptions, IAWSCredentialsFactory credentialsFactory, ILoggerFactory loggerFactory, IConfiguration configuration) { _serviceInterfaceType = type; _awsOptions = awsOptions; + _credentialsFactory = credentialsFactory; + _loggerFactory = loggerFactory; + _configuration = configuration; } /// @@ -53,24 +59,22 @@ internal ClientFactory(Type type, AWSOptions awsOptions) /// /// The dependency injection provider. /// The AWS service client - internal object CreateServiceClient(IServiceProvider provider) + internal object CreateServiceClient() { - var loggerFactory = provider.GetService(); - var logger = loggerFactory?.CreateLogger("AWSSDK"); + var logger = _loggerFactory?.CreateLogger("AWSSDK"); - var options = _awsOptions ?? provider.GetService(); - if(options == null) + var options = _awsOptions; + if(_awsOptions == null) { - var configuration = provider.GetService(); - if(configuration != null) + if(_configuration != null) { - options = configuration.GetAWSOptions(); + options = _configuration.GetAWSOptions(); if (options != null) logger?.LogInformation("Found AWS options in IConfiguration"); } } - return CreateServiceClient(logger, _serviceInterfaceType, options); + return CreateServiceClient(logger, _serviceInterfaceType, options, _credentialsFactory); } /// @@ -79,10 +83,10 @@ internal object CreateServiceClient(IServiceProvider provider) /// /// The dependency injection provider. /// The AWS service client - internal static IAmazonService CreateServiceClient(ILogger logger, Type serviceInterfaceType, AWSOptions options) + internal static IAmazonService CreateServiceClient(ILogger logger, Type serviceInterfaceType, AWSOptions options, IAWSCredentialsFactory credentialsFactory) { PerformGlobalConfig(logger, options); - var credentials = CreateCredentials(logger, options); + var credentials = credentialsFactory.Create(); if (!string.IsNullOrEmpty(options?.SessionRoleArn)) { @@ -160,51 +164,6 @@ private static AmazonServiceClient CreateClient(Type serviceInterfaceType, AWSCr return constructor.Invoke(new object[] { credentials, config }) as AmazonServiceClient; } - /// - /// Creates the AWSCredentials using either the profile indicated from the AWSOptions object - /// of the SDK fallback credentials search. - /// - /// - /// - private static AWSCredentials CreateCredentials(ILogger logger, AWSOptions options) - { - if (options != null) - { - if (options.Credentials != null) - { - logger?.LogInformation("Using AWS credentials specified with the AWSOptions.Credentials property"); - return options.Credentials; - } - if (!string.IsNullOrEmpty(options.Profile)) - { - var chain = new CredentialProfileStoreChain(options.ProfilesLocation); - AWSCredentials result; - if (chain.TryGetAWSCredentials(options.Profile, out result)) - { - logger?.LogInformation($"Found AWS credentials for the profile {options.Profile}"); - return result; - } - else - { - logger?.LogInformation($"Failed to find AWS credentials for the profile {options.Profile}"); - } - } - } - - var credentials = FallbackCredentialsFactory.GetCredentials(); - if (credentials == null) - { - logger?.LogError("Last effort to find AWS Credentials with AWS SDK's default credential search failed"); - throw new AmazonClientException("Failed to find AWS Credentials for constructing AWS service client"); - } - else - { - logger?.LogInformation("Found credentials using the AWS SDK's default credential search"); - } - - return credentials; - } - /// /// Creates the ClientConfig object for the service client. /// diff --git a/extensions/src/AWSSDK.Extensions.NETCore.Setup/DefaultAWSCredentialsFactory.cs b/extensions/src/AWSSDK.Extensions.NETCore.Setup/DefaultAWSCredentialsFactory.cs new file mode 100644 index 000000000000..dccfc0d15686 --- /dev/null +++ b/extensions/src/AWSSDK.Extensions.NETCore.Setup/DefaultAWSCredentialsFactory.cs @@ -0,0 +1,69 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +using Amazon.Extensions.NETCore.Setup; +using Amazon.Runtime; +using Amazon.Runtime.CredentialManagement; +using Microsoft.Extensions.Logging; + +namespace AWSSDK.Extensions.NETCore.Setup +{ + public class DefaultAWSCredentialsFactory : IAWSCredentialsFactory + { + private readonly AWSOptions _options; + private readonly ILogger _logger; + + public DefaultAWSCredentialsFactory(AWSOptions options, ILogger logger = null) + { + _options = options; + _logger = logger; + } + + /// + /// Creates the AWSCredentials using either AWSOptions.Credentials, AWSOptions.Profile + AWSOptions.ProfilesLocation, + /// or the SDK fallback credentials search. + /// + public AWSCredentials Create() + { + if (_options?.Credentials != null) + { + _logger?.LogInformation("Using AWS credentials specified with the AWSOptions.Credentials property"); + return _options.Credentials; + } + + if (!string.IsNullOrWhiteSpace(_options?.Profile)) + { + var chain = new CredentialProfileStoreChain(_options.ProfilesLocation); + if (chain.TryGetAWSCredentials(_options.Profile, out var result)) + { + _logger?.LogInformation("Found AWS credentials for the profile {OptionsProfile}", _options.Profile); + return result; + } + + _logger?.LogInformation("Failed to find AWS credentials for the profile {OptionsProfile}", _options.Profile); + } + + var credentials = FallbackCredentialsFactory.GetCredentials(); + if (credentials == null) + { + _logger?.LogError("Last effort to find AWS Credentials with AWS SDK's default credential search failed"); + throw new AmazonClientException("Failed to find AWS Credentials for constructing AWS service client"); + } + + _logger?.LogInformation("Found credentials using the AWS SDK's default credential search"); + + return credentials; + } + } +} \ No newline at end of file diff --git a/extensions/src/AWSSDK.Extensions.NETCore.Setup/IAWSCredentialsFactory.cs b/extensions/src/AWSSDK.Extensions.NETCore.Setup/IAWSCredentialsFactory.cs new file mode 100644 index 000000000000..b9adf3a9d364 --- /dev/null +++ b/extensions/src/AWSSDK.Extensions.NETCore.Setup/IAWSCredentialsFactory.cs @@ -0,0 +1,23 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +using Amazon.Runtime; + +namespace AWSSDK.Extensions.NETCore.Setup +{ + public interface IAWSCredentialsFactory + { + AWSCredentials Create(); + } +} \ No newline at end of file diff --git a/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs b/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs index 242092db89bc..ece52ca0a836 100644 --- a/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs +++ b/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs @@ -13,15 +13,12 @@ * permissions and limitations under the License. */ using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -using Microsoft.Extensions.DependencyInjection; - using Amazon.Runtime; using Amazon.Extensions.NETCore.Setup; +using AWSSDK.Extensions.NETCore.Setup; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; namespace Microsoft.Extensions.DependencyInjection { @@ -43,8 +40,7 @@ public static class ServiceCollectionExtensions /// Returns back the IServiceCollection to continue the fluent system of IServiceCollection. public static IServiceCollection AddDefaultAWSOptions(this IServiceCollection collection, AWSOptions options) { - collection.Add(new ServiceDescriptor(typeof(AWSOptions), options)); - return collection; + return collection.AddDefaultAWSOptions(_ => options); } /// @@ -66,6 +62,38 @@ public static IServiceCollection AddDefaultAWSOptions( return collection; } + /// + /// Adds the DefaultAWSCredentialsFactory object to the dependency injection framework + /// This factory will be used to create the credentials for the Amazon service clients. + /// + public static IServiceCollection AddCredentialsFactory(this IServiceCollection collection) + { + return collection.AddCredentialsFactoryInternal(); + } + + /// + /// Adds the DefaultAWSCredentialsFactory object to the dependency injection framework + /// if no IAWSCredentialsFactory is already registered. + /// This factory will be used to create the credentials for the Amazon service clients. + /// + public static IServiceCollection TryAddCredentialsFactory(this IServiceCollection collection) + { + return collection.AddCredentialsFactoryInternal(tryAdd: true); + } + + /// + /// Adds a IAWSCredentialsFactory object obtained via th provided awsCredentialsFactoryFunc to the dependency + /// injection framework. This factory will be used to create the credentials for the Amazon service clients. + /// + public static IServiceCollection AddCredentialsFactory( + this IServiceCollection collection, + Func awsCredentialsFactoryFunc, + ServiceLifetime lifetime = ServiceLifetime.Singleton, + bool tryAdd = false) + { + return collection.AddCredentialsFactoryInternal(awsCredentialsFactoryFunc, lifetime, tryAdd); + } + /// /// Adds the Amazon service client to the dependency injection framework. The Amazon service client is not /// created until it is requested. If the ServiceLifetime property is set to Singleton, the default, then the same @@ -77,7 +105,7 @@ public static IServiceCollection AddDefaultAWSOptions( /// Returns back the IServiceCollection to continue the fluent system of IServiceCollection. public static IServiceCollection AddAWSService(this IServiceCollection collection, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService { - return AddAWSService(collection, null, lifetime); + return collection.AddAWSServiceInternal(lifetime: lifetime); } /// @@ -89,15 +117,38 @@ public static IServiceCollection AddAWSService(this IServiceCollection collec /// /// 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. + /// A func which takes an IServiceProvider and the AWSOptions resolved via the optionsFunc and + /// returns the IAWSCredentialsFactory used to create the service client overriding the default added using AddCredentialsFactory. If none is provided, the registered IAWSCredentialsFactory will be used, + /// and if none has been registered a new instance of DefaultAWSCredentialsFactory will be used. + /// /// Returns back the IServiceCollection to continue the fluent system of IServiceCollection. - public static IServiceCollection AddAWSService(this IServiceCollection collection, AWSOptions options, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService + public static IServiceCollection AddAWSService(this IServiceCollection collection, AWSOptions options, ServiceLifetime lifetime = ServiceLifetime.Singleton, Func credentialsFactoryFunc = null) where T : IAmazonService { - Func factory = - new ClientFactory(typeof(T), options).CreateServiceClient; + return collection.AddAWSServiceInternal(_ => options, credentialsFactoryFunc: credentialsFactoryFunc, lifetime: lifetime); + } - var descriptor = new ServiceDescriptor(typeof(T), factory, lifetime); - collection.Add(descriptor); - return collection; + /// + /// Adds the Amazon service client to the dependency injection framework. 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. + /// + /// A func which returns the AWS options used to create the service client overriding the default AWS options added using AddDefaultAWSOptions. + /// A func which takes an IServiceProvider and the AWSOptions resolved via the optionsFunc and + /// returns the IAWSCredentialsFactory used to create the service client overriding the default added using AddCredentialsFactory. If none is provided, the registered IAWSCredentialsFactory will be used, + /// and if none has been registered a new instance of DefaultAWSCredentialsFactory will be used. + /// + /// 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 AddAWSService( + this IServiceCollection collection, + Func optionsFunc, + Func credentialsFactoryFunc = null, + ServiceLifetime lifetime = ServiceLifetime.Singleton) + where T : IAmazonService + { + return collection.AddAWSServiceInternal(optionsFunc, credentialsFactoryFunc, lifetime); } /// @@ -111,7 +162,7 @@ public static IServiceCollection AddAWSService(this IServiceCollection collec /// Returns back the IServiceCollection to continue the fluent system of IServiceCollection. public static IServiceCollection TryAddAWSService(this IServiceCollection collection, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService { - return TryAddAWSService(collection, null, lifetime); + return collection.AddAWSServiceInternal(lifetime: lifetime, tryAdd: true); } /// @@ -123,15 +174,129 @@ public static IServiceCollection TryAddAWSService(this IServiceCollection col /// /// 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. + /// /// A func which takes an IServiceProvider and the AWSOptions resolved via the optionsFunc and + /// returns the IAWSCredentialsFactory used to create the service client overriding the default added using AddCredentialsFactory. If none is provided, the registered IAWSCredentialsFactory will be used, + /// and if none has been registered a new instance of DefaultAWSCredentialsFactory will be used. + /// /// Returns back the IServiceCollection to continue the fluent system of IServiceCollection. - public static IServiceCollection TryAddAWSService(this IServiceCollection collection, AWSOptions options, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService + public static IServiceCollection TryAddAWSService(this IServiceCollection collection, AWSOptions options, ServiceLifetime lifetime = ServiceLifetime.Singleton, Func credentialsFactoryFunc = null) where T : IAmazonService { - Func factory = - new ClientFactory(typeof(T), options).CreateServiceClient; + return collection.AddAWSServiceInternal(_ => options, lifetime: lifetime, credentialsFactoryFunc: credentialsFactoryFunc, tryAdd: true); + } + + /// + /// Adds the Amazon service client to the dependency injection framework if the service type hasn't already been registered. + /// 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. + /// + /// A func which returns the AWS options used to create the service client overriding the default AWS options added using AddDefaultAWSOptions. + /// A func which takes an IServiceProvider and the AWSOptions resolved via the optionsFunc and + /// returns the IAWSCredentialsFactory used to create the service client overriding the default added using AddCredentialsFactory. If none is provided, the registered IAWSCredentialsFactory will be used, + /// and if none has been registered a new instance of DefaultAWSCredentialsFactory will be used. + /// + /// 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 TryAddAWSService( + this IServiceCollection collection, + Func optionsFunc, + Func credentialsFactoryFunc = null, + ServiceLifetime lifetime = ServiceLifetime.Singleton) + where T : IAmazonService + { + return collection.AddAWSServiceInternal(optionsFunc, credentialsFactoryFunc, lifetime, tryAdd: true); + } + + private static IServiceCollection AddCredentialsFactoryInternal( + this IServiceCollection collection, + Func awsCredentialsFactoryFunc = null, + ServiceLifetime lifetime = ServiceLifetime.Singleton, + bool tryAdd = false) + { + if (awsCredentialsFactoryFunc != null) + { + collection.SetCustomCredentialsFactoryRegistered(); + } + + awsCredentialsFactoryFunc = awsCredentialsFactoryFunc ?? (sp => new DefaultAWSCredentialsFactory(sp.GetService(), sp.GetService()?.CreateLogger())); + + var serviceDescriptor = new ServiceDescriptor(typeof(IAWSCredentialsFactory), awsCredentialsFactoryFunc, lifetime); + + if (tryAdd) + { + collection.TryAdd(serviceDescriptor); + } + else + { + collection.Add(serviceDescriptor); + } + + collection.TryAddTransient(sp => sp.GetRequiredService().Create()); - var descriptor = new ServiceDescriptor(typeof(T), factory, lifetime); - collection.TryAdd(descriptor); return collection; } + + private static IServiceCollection AddAWSServiceInternal( + this IServiceCollection collection, + Func optionsFunc = null, + Func credentialsFactoryFunc = null, + ServiceLifetime lifetime = ServiceLifetime.Singleton, + bool tryAdd = false) + where T : IAmazonService + { + var descriptor = new ServiceDescriptor( + typeof(T), + sp => CreateServiceClient(sp, optionsFunc, credentialsFactoryFunc), + lifetime); + + if (tryAdd) + { + collection.TryAdd(descriptor); + } + else + { + collection.Add(descriptor); + } + + return collection; + } + + private static object CreateServiceClient( + IServiceProvider sp, + Func optionsFunc = null, + Func credentialsFactoryFunc = null) + { + var options = optionsFunc?.Invoke(sp) + ?? sp.GetService() + ?? sp.GetService()?.GetAWSOptions(); + + if (optionsFunc != null && credentialsFactoryFunc == null && sp.CustomCredentialsFactoryRegistered() && options?.Equals(sp.GetService()) != true) + { + throw new ArgumentNullException( + nameof(credentialsFactoryFunc), + "A credentialsFactoryFunc must be provided when options(Func) is provided and [Try]AddCredentialsFactory is called with a custom IAWSCredentialsFactory. " + + "Not doing this would result in the ServiceClient using a different AWSOptions object than the IAWSCredentialsFactory"); + } + + var awsCredentialsFactory = credentialsFactoryFunc?.Invoke(sp, options) + ?? sp.GetService() + ?? new DefaultAWSCredentialsFactory(options, sp.GetService()?.CreateLogger()); + + var clientFactory = new ClientFactory( + typeof(T), + options, + awsCredentialsFactory, + sp.GetService(), + sp.GetService()); + + return clientFactory.CreateServiceClient(); + } + + private static bool CustomCredentialsFactoryRegistered(this IServiceProvider sp) => sp.GetService() != null; + + private static void SetCustomCredentialsFactoryRegistered(this IServiceCollection collection) => collection.AddSingleton(); + + private class CustomCredentialsFactoryRegisteredType { } } } From f7e2580315de2272139fe6f2b621954fb9a4b949 Mon Sep 17 00:00:00 2001 From: Chase Miller Date: Thu, 20 Mar 2025 16:23:58 -0400 Subject: [PATCH 2/5] Add DI Credentials Factory tests --- .../ServiceCollectionExtensions.cs | 12 +- .../DependencyInjectionTests.cs | 279 ++++++++++++++++++ 2 files changed, 288 insertions(+), 3 deletions(-) diff --git a/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs b/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs index ece52ca0a836..da5d97eb6cb6 100644 --- a/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs +++ b/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs @@ -219,7 +219,7 @@ private static IServiceCollection AddCredentialsFactoryInternal( collection.SetCustomCredentialsFactoryRegistered(); } - awsCredentialsFactoryFunc = awsCredentialsFactoryFunc ?? (sp => new DefaultAWSCredentialsFactory(sp.GetService(), sp.GetService()?.CreateLogger())); + awsCredentialsFactoryFunc = awsCredentialsFactoryFunc ?? (sp => sp.CreateDefaultCredentialsFactory(sp.GetService())); var serviceDescriptor = new ServiceDescriptor(typeof(IAWSCredentialsFactory), awsCredentialsFactoryFunc, lifetime); @@ -271,7 +271,10 @@ private static object CreateServiceClient( ?? sp.GetService() ?? sp.GetService()?.GetAWSOptions(); - if (optionsFunc != null && credentialsFactoryFunc == null && sp.CustomCredentialsFactoryRegistered() && options?.Equals(sp.GetService()) != true) + if (optionsFunc != null && + credentialsFactoryFunc == null && + sp.CustomCredentialsFactoryRegistered() && // If we're using the default credentials factory, no harm done since we're creating one with the provided AWSOptions below. + options?.Equals(sp.GetService()) != true) { throw new ArgumentNullException( nameof(credentialsFactoryFunc), @@ -281,7 +284,7 @@ private static object CreateServiceClient( var awsCredentialsFactory = credentialsFactoryFunc?.Invoke(sp, options) ?? sp.GetService() - ?? new DefaultAWSCredentialsFactory(options, sp.GetService()?.CreateLogger()); + ?? sp.CreateDefaultCredentialsFactory(options); var clientFactory = new ClientFactory( typeof(T), @@ -293,6 +296,9 @@ private static object CreateServiceClient( return clientFactory.CreateServiceClient(); } + private static IAWSCredentialsFactory CreateDefaultCredentialsFactory(this IServiceProvider sp, AWSOptions options) => + new DefaultAWSCredentialsFactory(options, sp.GetService()?.CreateLogger()); + private static bool CustomCredentialsFactoryRegistered(this IServiceProvider sp) => sp.GetService() != null; private static void SetCustomCredentialsFactoryRegistered(this IServiceCollection collection) => collection.AddSingleton(); diff --git a/extensions/test/NETCore.SetupTests/DependencyInjectionTests.cs b/extensions/test/NETCore.SetupTests/DependencyInjectionTests.cs index d1c38275cb47..d84d9c91fd32 100644 --- a/extensions/test/NETCore.SetupTests/DependencyInjectionTests.cs +++ b/extensions/test/NETCore.SetupTests/DependencyInjectionTests.cs @@ -8,6 +8,9 @@ using Amazon.Extensions.NETCore.Setup; using Moq; using System; +using Amazon.Runtime; +using AWSSDK.Extensions.NETCore.Setup; +using Castle.Core.Logging; namespace DependencyInjectionTests { @@ -149,6 +152,282 @@ public void InjectS3ClientWithFactoryBuiltConfig() Assert.Equal(expectRegion, controller.S3Client.Config.RegionEndpoint); } + [Fact] + public void InjectS3ClientWithOverridingConfigAndCustomCredentialsProviderFunc() + { + var builder = new ConfigurationBuilder(); + builder.AddJsonFile("./TestFiles/GetClientConfigSettingsTest.json"); + + IConfiguration config = builder.Build(); + + ServiceCollection services = new ServiceCollection(); + services.AddDefaultAWSOptions(config.GetAWSOptions()); + services.AddAWSService( + new AWSOptions {Region = RegionEndpoint.EUCentral1 }, + credentialsFactoryFunc: (sp, options) => new DefaultAWSCredentialsFactory(options, Mock.Of>())); + + var serviceProvider = services.BuildServiceProvider(); + + var controller = ActivatorUtilities.CreateInstance(serviceProvider); + Assert.NotNull(controller.S3Client); + Assert.Equal(RegionEndpoint.EUCentral1, controller.S3Client.Config.RegionEndpoint); + } + + [Fact] + public void GivenCustomAddCredentialsFactoryCall_WhenInjectingS3Client_ThenUseCustomCredentials() + { + var builder = new ConfigurationBuilder(); + builder.AddJsonFile("./TestFiles/GetClientConfigSettingsTest.json"); + + IConfiguration config = builder.Build(); + + var mockCredentialsFactory = new Mock(); + mockCredentialsFactory + .Setup(x => x.Create()) + .Returns(new BasicAWSCredentials("test", "test")); + + ServiceCollection services = new ServiceCollection(); + services.AddDefaultAWSOptions(config.GetAWSOptions()); + services.AddCredentialsFactory(_ => mockCredentialsFactory.Object); + services.AddAWSService(); + + var serviceProvider = services.BuildServiceProvider(); + + var controller = ActivatorUtilities.CreateInstance(serviceProvider); + Assert.NotNull(controller.S3Client); + Assert.Equal(RegionEndpoint.USWest2, controller.S3Client.Config.RegionEndpoint); + mockCredentialsFactory.Verify(x => x.Create(), Times.Once); + } + + [Fact] + public void GivenCustomAddCredentialsFactory_WhenInjectingS3ClientWithOverridingConfigAndCustomCredentialsProviderFunc_ThenUseOverride() + { + var builder = new ConfigurationBuilder(); + builder.AddJsonFile("./TestFiles/GetClientConfigSettingsTest.json"); + + IConfiguration config = builder.Build(); + + var mockCredentialsFactory = new Mock(); + mockCredentialsFactory + .Setup(x => x.Create()) + .Returns(new BasicAWSCredentials("test", "test")); + + var mockOverridenCredentialsFactory = new Mock(); + mockOverridenCredentialsFactory + .Setup(x => x.Create()) + .Returns(new BasicAWSCredentials("overriden", "overriden")); + + var awsOptions = new AWSOptions {Region = RegionEndpoint.EUCentral1 }; + AWSOptions providedOptionsToCredentialsFactoryFunc = new AWSOptions(); + + ServiceCollection services = new ServiceCollection(); + services.AddDefaultAWSOptions(config.GetAWSOptions()); + services.AddCredentialsFactory(sp => mockCredentialsFactory.Object); + services.AddAWSService( + awsOptions, + credentialsFactoryFunc: (sp, options) => + { + providedOptionsToCredentialsFactoryFunc = options; + return mockOverridenCredentialsFactory.Object; + }); + + var serviceProvider = services.BuildServiceProvider(); + + var controller = ActivatorUtilities.CreateInstance(serviceProvider); + Assert.NotNull(controller.S3Client); + Assert.Equal(RegionEndpoint.EUCentral1, controller.S3Client.Config.RegionEndpoint); + Assert.Equal(providedOptionsToCredentialsFactoryFunc, awsOptions); + mockOverridenCredentialsFactory.Verify(x => x.Create(), Times.Once); + mockCredentialsFactory.Verify(x => x.Create(), Times.Never); + } + + [Fact] + public void GivenCustomAddCredentialsFactory_WhenInjectingS3ClientWithOverridingConfigAndNoCustomCredentialsProviderFunc_ThrowException() + { + var builder = new ConfigurationBuilder(); + builder.AddJsonFile("./TestFiles/GetClientConfigSettingsTest.json"); + + IConfiguration config = builder.Build(); + + var mockCredentialsFactory = new Mock(); + mockCredentialsFactory + .Setup(x => x.Create()) + .Returns(new BasicAWSCredentials("test", "test")); + + ServiceCollection services = new ServiceCollection(); + services.AddDefaultAWSOptions(config.GetAWSOptions()); + services.AddCredentialsFactory(sp => mockCredentialsFactory.Object); + services.AddAWSService(new AWSOptions {Region = RegionEndpoint.EUCentral1 }); + + var serviceProvider = services.BuildServiceProvider(); + + Func controllerAction = () => ActivatorUtilities.CreateInstance(serviceProvider); + Assert.Throws(controllerAction); + mockCredentialsFactory.Verify(x => x.Create(), Times.Never); + } + + [Fact] + public void GivenDefaultAddCredentialsFactory_WhenInjectingS3ClientWithSameConfigAsRegisteredAndNoCustomCredentialsProviderFunc_ThenWork() + { + var builder = new ConfigurationBuilder(); + builder.AddJsonFile("./TestFiles/GetClientConfigSettingsTest.json"); + + IConfiguration config = builder.Build(); + + var awsOptions = new AWSOptions {Region = RegionEndpoint.EUCentral1 }; + var mockCredentialsFactory = new Mock(); + mockCredentialsFactory + .Setup(x => x.Create()) + .Returns(new BasicAWSCredentials("test", "test")); + + ServiceCollection services = new ServiceCollection(); + services.AddDefaultAWSOptions(awsOptions); + services.AddCredentialsFactory(_ => mockCredentialsFactory.Object); + services.AddAWSService(awsOptions); + + var serviceProvider = services.BuildServiceProvider(); + + var controller = ActivatorUtilities.CreateInstance(serviceProvider); + Assert.NotNull(controller.S3Client); + Assert.Equal(RegionEndpoint.EUCentral1, controller.S3Client.Config.RegionEndpoint); + mockCredentialsFactory.Verify(x => x.Create(), Times.Once); + } + + [Fact] + public void GivenDefaultAddCredentialsFactoryCall_WhenInjectingS3ClientWithOverridingConfigAndNoCustomCredentialsProviderFunc_ThenWork() + { + var builder = new ConfigurationBuilder(); + builder.AddJsonFile("./TestFiles/GetClientConfigSettingsTest.json"); + + IConfiguration config = builder.Build(); + + ServiceCollection services = new ServiceCollection(); + services.AddDefaultAWSOptions(config.GetAWSOptions()); + services.AddCredentialsFactory(); + services.AddAWSService(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 GivenDefaultAddCredentialsFactoryCall_WhenInjectingS3Client_ThenWork() + { + var builder = new ConfigurationBuilder(); + builder.AddJsonFile("./TestFiles/GetClientConfigSettingsTest.json"); + + IConfiguration config = builder.Build(); + + ServiceCollection services = new ServiceCollection(); + services.AddDefaultAWSOptions(config.GetAWSOptions()); + services.AddCredentialsFactory(); + services.AddAWSService(); + + var serviceProvider = services.BuildServiceProvider(); + + var controller = ActivatorUtilities.CreateInstance(serviceProvider); + Assert.NotNull(controller.S3Client); + Assert.Equal(RegionEndpoint.USWest2, controller.S3Client.Config.RegionEndpoint); + } + + [Fact] + public void GivenNoAddCredentialsFactoryCall_WhenInjectingS3ClientWithOverridingConfigAndNoCustomCredentialsProviderFunc_ThenUseDefault() + { + var builder = new ConfigurationBuilder(); + builder.AddJsonFile("./TestFiles/GetClientConfigSettingsTest.json"); + + IConfiguration config = builder.Build(); + + ServiceCollection services = new ServiceCollection(); + services.AddDefaultAWSOptions(config.GetAWSOptions()); + services.AddAWSService(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 GivenNoAddCredentialsFactoryCall_WhenInjectingS3ClientWithNoOverridingConfigAndNoCustomCredentialsProviderFunc_ThenUseDefault() + { + var builder = new ConfigurationBuilder(); + builder.AddJsonFile("./TestFiles/GetClientConfigSettingsTest.json"); + + IConfiguration config = builder.Build(); + + ServiceCollection services = new ServiceCollection(); + services.AddDefaultAWSOptions(config.GetAWSOptions()); + services.AddAWSService(); + + var serviceProvider = services.BuildServiceProvider(); + + var controller = ActivatorUtilities.CreateInstance(serviceProvider); + Assert.NotNull(controller.S3Client); + Assert.Equal(RegionEndpoint.USWest2, controller.S3Client.Config.RegionEndpoint); + } + + [Fact] + public void GivenNoAddCredentialsFactoryCall_WhenInjectingS3ClientWithNoOverridingConfigButCustomCredentialsProviderFunc_ThenUseCustom() + { + var builder = new ConfigurationBuilder(); + builder.AddJsonFile("./TestFiles/GetClientConfigSettingsTest.json"); + + IConfiguration config = builder.Build(); + + var mockOverridenCredentialsFactory = new Mock(); + mockOverridenCredentialsFactory + .Setup(x => x.Create()) + .Returns(new BasicAWSCredentials("overriden", "overriden")); + + ServiceCollection services = new ServiceCollection(); + services.AddDefaultAWSOptions(config.GetAWSOptions()); + services.AddAWSService((AWSOptions)null, credentialsFactoryFunc: (sp, options) => mockOverridenCredentialsFactory.Object); + + var serviceProvider = services.BuildServiceProvider(); + + var controller = ActivatorUtilities.CreateInstance(serviceProvider); + Assert.NotNull(controller.S3Client); + Assert.Equal(RegionEndpoint.USWest2, controller.S3Client.Config.RegionEndpoint); + mockOverridenCredentialsFactory.Verify(x => x.Create(), Times.Once); + } + + [Fact] + public void GivenCustomAddCredentialsFactoryCall_WhenInjectingS3ClientWithNoOverridingConfigButCustomCredentialsProviderFunc_ThenUseCustom() + { + var builder = new ConfigurationBuilder(); + builder.AddJsonFile("./TestFiles/GetClientConfigSettingsTest.json"); + + IConfiguration config = builder.Build(); + + var mockCredentialsFactory = new Mock(); + mockCredentialsFactory + .Setup(x => x.Create()) + .Returns(new BasicAWSCredentials("test", "test")); + + var mockOverridenCredentialsFactory = new Mock(); + mockOverridenCredentialsFactory + .Setup(x => x.Create()) + .Returns(new BasicAWSCredentials("overriden", "overriden")); + + ServiceCollection services = new ServiceCollection(); + services.AddDefaultAWSOptions(config.GetAWSOptions()); + services.AddCredentialsFactory(_ => mockCredentialsFactory.Object); + services.AddAWSService((AWSOptions)null, credentialsFactoryFunc: (sp, options) => mockOverridenCredentialsFactory.Object); + + var serviceProvider = services.BuildServiceProvider(); + + var controller = ActivatorUtilities.CreateInstance(serviceProvider); + Assert.NotNull(controller.S3Client); + Assert.Equal(RegionEndpoint.USWest2, controller.S3Client.Config.RegionEndpoint); + mockOverridenCredentialsFactory.Verify(x => x.Create(), Times.Once); + mockCredentialsFactory.Verify(x => x.Create(), Times.Never); + } + public class TestController { public IAmazonS3 S3Client { get; private set; } From 45b2c56990fa0ac4da4771c884fa7c8d86a1def7 Mon Sep 17 00:00:00 2001 From: Chase Miller Date: Thu, 20 Mar 2025 16:59:18 -0400 Subject: [PATCH 3/5] Add TryAddCredentialsFactory overload --- .../ServiceCollectionExtensions.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs b/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs index da5d97eb6cb6..bcdd770ad42b 100644 --- a/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs +++ b/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs @@ -88,10 +88,22 @@ public static IServiceCollection TryAddCredentialsFactory(this IServiceCollectio public static IServiceCollection AddCredentialsFactory( this IServiceCollection collection, Func awsCredentialsFactoryFunc, - ServiceLifetime lifetime = ServiceLifetime.Singleton, - bool tryAdd = false) + ServiceLifetime lifetime = ServiceLifetime.Singleton) + { + return collection.AddCredentialsFactoryInternal(awsCredentialsFactoryFunc, lifetime, tryAdd: false); + } + + /// + /// Adds a IAWSCredentialsFactory object obtained via th provided awsCredentialsFactoryFunc to the dependency + /// injection framework if no IAWSCredentialsFactory is already registered. + /// This factory will be used to create the credentials for the Amazon service clients. + /// + public static IServiceCollection TryAddCredentialsFactory( + this IServiceCollection collection, + Func awsCredentialsFactoryFunc, + ServiceLifetime lifetime = ServiceLifetime.Singleton) { - return collection.AddCredentialsFactoryInternal(awsCredentialsFactoryFunc, lifetime, tryAdd); + return collection.AddCredentialsFactoryInternal(awsCredentialsFactoryFunc, lifetime, tryAdd: true); } /// From 7393275f40d74160f475de92814d8ea1edad1718 Mon Sep 17 00:00:00 2001 From: Chase Miller Date: Thu, 20 Mar 2025 17:07:36 -0400 Subject: [PATCH 4/5] Update xml documentation --- .../ServiceCollectionExtensions.cs | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs b/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs index bcdd770ad42b..07568458d376 100644 --- a/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs +++ b/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs @@ -129,9 +129,10 @@ public static IServiceCollection AddAWSService(this IServiceCollection collec /// /// 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. - /// A func which takes an IServiceProvider and the AWSOptions resolved via the optionsFunc and - /// returns the IAWSCredentialsFactory used to create the service client overriding the default added using AddCredentialsFactory. If none is provided, the registered IAWSCredentialsFactory will be used, - /// and if none has been registered a new instance of DefaultAWSCredentialsFactory will be used. + /// + /// A func which takes an IServiceProvider and the AWSOptions provided to this call and returns an IAWSCredentialsFactory used to create the service client. + /// Must be provided if options are provided to this call _and_ a custom IAWSCredentialsFactory is registered via [Try]AddCredentialsFactory; otherwise the + /// default will be used. /// /// Returns back the IServiceCollection to continue the fluent system of IServiceCollection. public static IServiceCollection AddAWSService(this IServiceCollection collection, AWSOptions options, ServiceLifetime lifetime = ServiceLifetime.Singleton, Func credentialsFactoryFunc = null) where T : IAmazonService @@ -147,9 +148,10 @@ public static IServiceCollection AddAWSService(this IServiceCollection collec /// The AWS service interface, like IAmazonS3. /// /// A func which returns the AWS options used to create the service client overriding the default AWS options added using AddDefaultAWSOptions. - /// A func which takes an IServiceProvider and the AWSOptions resolved via the optionsFunc and - /// returns the IAWSCredentialsFactory used to create the service client overriding the default added using AddCredentialsFactory. If none is provided, the registered IAWSCredentialsFactory will be used, - /// and if none has been registered a new instance of DefaultAWSCredentialsFactory will be used. + /// + /// A func which takes an IServiceProvider and the AWSOptions resolved by optionsFunc and returns an IAWSCredentialsFactory used to create the service client. + /// Must be provided if options are provided to this call _and_ a custom IAWSCredentialsFactory is registered via [Try]AddCredentialsFactory; otherwise the + /// default will be used. /// /// The lifetime of the service client created. The default is Singleton. /// Returns back the IServiceCollection to continue the fluent system of IServiceCollection. @@ -186,9 +188,10 @@ public static IServiceCollection TryAddAWSService(this IServiceCollection col /// /// 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. - /// /// A func which takes an IServiceProvider and the AWSOptions resolved via the optionsFunc and - /// returns the IAWSCredentialsFactory used to create the service client overriding the default added using AddCredentialsFactory. If none is provided, the registered IAWSCredentialsFactory will be used, - /// and if none has been registered a new instance of DefaultAWSCredentialsFactory will be used. + /// + /// A func which takes an IServiceProvider and the AWSOptions resolved by optionsFunc and returns an IAWSCredentialsFactory used to create the service client. + /// Must be provided if options are provided to this call _and_ a custom IAWSCredentialsFactory is registered via [Try]AddCredentialsFactory; otherwise the + /// default will be used. /// /// Returns back the IServiceCollection to continue the fluent system of IServiceCollection. public static IServiceCollection TryAddAWSService(this IServiceCollection collection, AWSOptions options, ServiceLifetime lifetime = ServiceLifetime.Singleton, Func credentialsFactoryFunc = null) where T : IAmazonService @@ -204,9 +207,10 @@ public static IServiceCollection TryAddAWSService(this IServiceCollection col /// The AWS service interface, like IAmazonS3. /// /// A func which returns the AWS options used to create the service client overriding the default AWS options added using AddDefaultAWSOptions. - /// A func which takes an IServiceProvider and the AWSOptions resolved via the optionsFunc and - /// returns the IAWSCredentialsFactory used to create the service client overriding the default added using AddCredentialsFactory. If none is provided, the registered IAWSCredentialsFactory will be used, - /// and if none has been registered a new instance of DefaultAWSCredentialsFactory will be used. + /// + /// A func which takes an IServiceProvider and the AWSOptions resolved by optionsFunc and returns an IAWSCredentialsFactory used to create the service client. + /// Must be provided if options are provided to this call _and_ a custom IAWSCredentialsFactory is registered via [Try]AddCredentialsFactory; otherwise the + /// default will be used. /// /// The lifetime of the service client created. The default is Singleton. /// Returns back the IServiceCollection to continue the fluent system of IServiceCollection. From 288b90ea0b1b95ae49ab12b8c24d9721d0d29569 Mon Sep 17 00:00:00 2001 From: Chase Miller Date: Tue, 1 Apr 2025 13:03:52 -0400 Subject: [PATCH 5/5] Remove AWSCredentials registration since its disposal by the framework breaks FallbackCredentialsFactory's assumptions RefreshingAWSCredentials will throw an exception if it's been disposed of (and used via FallbackCredentialsFactory) --- .../ServiceCollectionExtensions.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs b/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs index 07568458d376..dec641eba33b 100644 --- a/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs +++ b/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs @@ -248,8 +248,6 @@ private static IServiceCollection AddCredentialsFactoryInternal( collection.Add(serviceDescriptor); } - collection.TryAddTransient(sp => sp.GetRequiredService().Create()); - return collection; }