diff --git a/extensions/src/AWSSDK.Extensions.NETCore.Setup/AWSOptions.cs b/extensions/src/AWSSDK.Extensions.NETCore.Setup/AWSOptions.cs index e137c66fd175..21e9e2611f75 100644 --- a/extensions/src/AWSSDK.Extensions.NETCore.Setup/AWSOptions.cs +++ b/extensions/src/AWSSDK.Extensions.NETCore.Setup/AWSOptions.cs @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ using Amazon.Runtime; - +using AWSSDK.Extensions.NETCore.Setup; using Microsoft.Extensions.Logging; namespace Amazon.Extensions.NETCore.Setup @@ -92,18 +92,6 @@ internal set /// public LoggingSetting Logging { get; set; } - /// - /// Create a service client for the specified service interface using the options set in this instance. - /// For example if T is set to IAmazonS3 then the AmazonS3ServiceClient which implements IAmazonS3 is created - /// and returned. - /// - /// The service interface that a service client will be created for. - /// The service client that implements the service interface. - public T CreateServiceClient() where T : class, IAmazonService - { - return new ClientFactory(this).CreateServiceClient((ILogger)null, this) as T; - } - /// /// Container for logging settings of the SDK /// diff --git a/extensions/src/AWSSDK.Extensions.NETCore.Setup/AWSOptionsExtensions.cs b/extensions/src/AWSSDK.Extensions.NETCore.Setup/AWSOptionsExtensions.cs new file mode 100644 index 000000000000..d7f4341f61f0 --- /dev/null +++ b/extensions/src/AWSSDK.Extensions.NETCore.Setup/AWSOptionsExtensions.cs @@ -0,0 +1,27 @@ +using Amazon.Extensions.NETCore.Setup; +using Amazon.Runtime; + +namespace AWSSDK.Extensions.NETCore.Setup +{ + /// + /// + /// + public static class AWSOptionsExtensions + { + /// + /// Create a service client for the specified service interface using the options set in this instance. + /// For example if T is set to IAmazonS3 then the AmazonS3ServiceClient which implements IAmazonS3 is created + /// and returned. + /// + /// The service interface that a service client will be created for. + /// The service client that implements the service interface. + public static T CreateServiceClient(this AWSOptions options) + where T : class, IAmazonService + { + var credentials = new DefaultAWSCredentials(options, null); + var clientFactory = new ClientFactory(options, credentials, null); + + return clientFactory.CreateServiceClient() as T; + } + } +} \ No newline at end of file diff --git a/extensions/src/AWSSDK.Extensions.NETCore.Setup/ClientFactory.cs b/extensions/src/AWSSDK.Extensions.NETCore.Setup/ClientFactory.cs index 26c402eada88..503fbee761c5 100644 --- a/extensions/src/AWSSDK.Extensions.NETCore.Setup/ClientFactory.cs +++ b/extensions/src/AWSSDK.Extensions.NETCore.Setup/ClientFactory.cs @@ -21,6 +21,7 @@ using Amazon.Runtime; using Amazon.Runtime.CredentialManagement; using Amazon.Runtime.Credentials.Internal; +using AWSSDK.Extensions.NETCore.Setup; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -36,68 +37,45 @@ internal class ClientFactory private static readonly Type[] EMPTY_TYPES = Array.Empty(); private static readonly object[] EMPTY_PARAMETERS = Array.Empty(); - private AWSOptions _awsOptions; + private readonly AWSOptions _options; + private readonly AWSCredentials _credentials; + private readonly ILogger _logger; /// /// Constructs an instance of the ClientFactory /// /// The AWS options used for creating service clients. - internal ClientFactory(AWSOptions awsOptions) - { - _awsOptions = awsOptions; - } - - /// - /// Creates the AWS service client that implements the service client interface. The AWSOptions object - /// will be searched for in the IServiceProvider. - /// - /// The dependency injection provider. - /// The AWS service client - internal object CreateServiceClient(IServiceProvider provider) + /// + /// + internal ClientFactory(AWSOptions awsOptions, AWSCredentials credentials, ILogger logger) { - var loggerFactory = provider.GetService(); - var logger = loggerFactory?.CreateLogger("AWSSDK"); - - var options = _awsOptions ?? provider.GetService(); - if(options == null) - { - var configuration = provider.GetService(); - if(configuration != null) - { - options = configuration.GetAWSOptions(); - if (options != null) - logger?.LogInformation("Found AWS options in IConfiguration"); - } - } - - return CreateServiceClient(logger, options); + _options = awsOptions ?? throw new ArgumentNullException(nameof(awsOptions)); + _credentials = credentials ?? throw new ArgumentNullException(nameof(credentials)); + _logger = logger; } /// - /// Creates the AWS service client that implements the service client interface. The AWSOptions object - /// will be searched for in the IServiceProvider. + /// Creates the AWS service client that implements the service client interface. /// - /// Logger instance for writing diagnostic logs. - /// The AWS options used for creating the service client. /// The AWS service client - internal IAmazonService CreateServiceClient(ILogger logger, AWSOptions options) + internal IAmazonService CreateServiceClient() { - PerformGlobalConfig(logger, options); - var credentials = CreateCredentials(logger, options); + PerformGlobalConfig(_logger, _options); + var credentials = _credentials; - if (!string.IsNullOrEmpty(options?.SessionRoleArn)) + if (!string.IsNullOrEmpty(_options?.SessionRoleArn)) { - if (string.IsNullOrEmpty(options?.ExternalId)) + if (string.IsNullOrEmpty(_options?.ExternalId)) { - credentials = new AssumeRoleAWSCredentials(credentials, options.SessionRoleArn, options.SessionName); + credentials = new AssumeRoleAWSCredentials(credentials, _options.SessionRoleArn, _options.SessionName); } else { - credentials = new AssumeRoleAWSCredentials(credentials, options.SessionRoleArn, options.SessionName, new AssumeRoleAWSCredentialsOptions() { ExternalId = options.ExternalId }); + credentials = new AssumeRoleAWSCredentials(credentials, _options.SessionRoleArn, _options.SessionName, new AssumeRoleAWSCredentialsOptions() { ExternalId = _options.ExternalId }); } } - var config = CreateConfig(options); + var config = CreateConfig(_options); var client = CreateClient(credentials, config); return client as IAmazonService; } @@ -165,52 +143,6 @@ private static AmazonServiceClient CreateClient(AWSCredentials credentials, Clie #endif } - /// - /// 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 = DefaultIdentityResolverConfiguration.ResolveDefaultIdentity(); - 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/DefaultAWSCredentials.cs b/extensions/src/AWSSDK.Extensions.NETCore.Setup/DefaultAWSCredentials.cs new file mode 100644 index 000000000000..7f7439187535 --- /dev/null +++ b/extensions/src/AWSSDK.Extensions.NETCore.Setup/DefaultAWSCredentials.cs @@ -0,0 +1,71 @@ +using Amazon.Extensions.NETCore.Setup; +using Amazon.Runtime; +using Amazon.Runtime.CredentialManagement; +using Amazon.Runtime.Credentials.Internal; +using Microsoft.Extensions.Logging; + +namespace AWSSDK.Extensions.NETCore.Setup +{ + /// + /// + /// + public class DefaultAWSCredentials : AWSCredentials + { + private readonly AWSOptions _options; + private readonly ILogger _logger; + + /// + /// + /// + /// + /// + public DefaultAWSCredentials(AWSOptions awsOptions, ILogger logger) + { + _options = awsOptions; + _logger = logger; + } + + /// + /// + /// + /// + public override ImmutableCredentials GetCredentials() + { + if (_options != null) + { + if (_options.Credentials != null) + { + _logger?.LogInformation("Using AWS credentials specified with the AWSOptions.Credentials property"); + return _options.Credentials.GetCredentials(); + } + 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.GetCredentials(); + } + else + { + _logger?.LogInformation($"Failed to find AWS credentials for the profile {_options.Profile}"); + } + } + } + + var credentials = DefaultIdentityResolverConfiguration.ResolveDefaultIdentity(); + 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.GetCredentials(); + } + } +} \ 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 c0cf329b5104..35658fcc78ed 100644 --- a/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs +++ b/extensions/src/AWSSDK.Extensions.NETCore.Setup/ServiceCollectionExtensions.cs @@ -21,7 +21,9 @@ using Amazon.Runtime; using Amazon.Extensions.NETCore.Setup; +using AWSSDK.Extensions.NETCore.Setup; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; namespace Microsoft.Extensions.DependencyInjection { @@ -63,6 +65,94 @@ public static IServiceCollection AddDefaultAWSOptions( return collection; } + /// + /// + /// + /// + /// + /// + public static IServiceCollection AddAWSCredentials( + this IServiceCollection collection, + ServiceLifetime lifetime = ServiceLifetime.Singleton) + { + collection.Add(new ServiceDescriptor(typeof(AWSCredentials), sp => sp.CreateDefaultAWSCredentials(), lifetime)); + return collection; + } + + /// + /// + /// + /// + /// + /// + public static IServiceCollection AddAWSCredentials( + this IServiceCollection collection, + AWSCredentials credentials) + { + collection.Add(new ServiceDescriptor(typeof(AWSCredentials), credentials)); + return collection; + } + + /// + /// + /// + /// + /// + /// + /// + public static IServiceCollection AddAWSCredentials( + this IServiceCollection collection, + Func credentialsFunc, + ServiceLifetime lifetime = ServiceLifetime.Singleton) + { + collection.Add(new ServiceDescriptor(typeof(AWSCredentials), credentialsFunc, lifetime)); + return collection; + } + + /// + /// + /// + /// + /// + /// + public static IServiceCollection TryAddAWSCredentials( + this IServiceCollection collection, + ServiceLifetime lifetime = ServiceLifetime.Singleton) + { + collection.TryAdd(new ServiceDescriptor(typeof(AWSCredentials), sp => sp.CreateDefaultAWSCredentials(), lifetime)); + return collection; + } + + /// + /// + /// + /// + /// + /// + public static IServiceCollection TryAddAWSCredentials( + this IServiceCollection collection, + AWSCredentials credentials) + { + collection.TryAdd(new ServiceDescriptor(typeof(AWSCredentials), credentials)); + return collection; + } + + /// + /// + /// + /// + /// + /// + /// + public static IServiceCollection TryAddAWSCredentials( + this IServiceCollection collection, + Func credentialsFunc, + ServiceLifetime lifetime = ServiceLifetime.Singleton) + { + collection.TryAdd(new ServiceDescriptor(typeof(AWSCredentials), credentialsFunc, lifetime)); + return collection; + } + /// /// Adds the AWSOptions object to the dependency injection framework providing information /// that will be used to construct Amazon service clients if they haven't already been registered. @@ -106,7 +196,7 @@ public static IServiceCollection TryAddDefaultAWSOptions( /// 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 AddAWSService(collection, options: null, lifetime); } /// @@ -121,10 +211,24 @@ public static IServiceCollection AddAWSService(this IServiceCollection collec /// 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 { - Func factory = - new ClientFactory(options).CreateServiceClient; + var descriptor = new ServiceDescriptor(typeof(T), sp => CreateServiceClient(options, sp), lifetime); + collection.Add(descriptor); + return collection; + } - var descriptor = new ServiceDescriptor(typeof(T), factory, lifetime); + /// + /// 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 that resolves 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 AddAWSService(this IServiceCollection collection, Func optionsFunc, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService + { + var descriptor = new ServiceDescriptor(typeof(T), sp => CreateServiceClient(optionsFunc(sp), sp), lifetime); collection.Add(descriptor); return collection; } @@ -140,7 +244,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 TryAddAWSService(collection, options: null, lifetime); } /// @@ -155,14 +259,28 @@ public static IServiceCollection TryAddAWSService(this IServiceCollection col /// 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 { - Func factory = - new ClientFactory(options).CreateServiceClient; + var descriptor = new ServiceDescriptor(typeof(T), sp => CreateServiceClient(options, sp), lifetime); + collection.TryAdd(descriptor); + return collection; + } - var descriptor = new ServiceDescriptor(typeof(T), factory, lifetime); + /// + /// 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 that resolves 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 TryAddAWSService(this IServiceCollection collection, Func optionsFunc, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService + { + var descriptor = new ServiceDescriptor(typeof(T), sp => CreateServiceClient(optionsFunc(sp), sp), lifetime); collection.TryAdd(descriptor); return collection; } - + #if NET8_0_OR_GREATER /// @@ -177,7 +295,7 @@ public static IServiceCollection TryAddAWSService(this IServiceCollection col /// 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); + return AddKeyedAWSService(collection, serviceKey, options: null, lifetime); } /// @@ -193,9 +311,25 @@ public static IServiceCollection AddKeyedAWSService(this IServiceCollection c /// 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, (sp, _) => CreateServiceClient(options, sp), lifetime); + collection.Add(descriptor); + return collection; + } - var descriptor = new ServiceDescriptor(typeof(T), serviceKey, Factory, 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. + /// A func that resolves 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, Func optionsFunc, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService + { + var descriptor = new ServiceDescriptor(typeof(T), serviceKey, (sp, _) => CreateServiceClient(optionsFunc(sp), sp), lifetime); collection.Add(descriptor); return collection; } @@ -212,7 +346,7 @@ public static IServiceCollection AddKeyedAWSService(this IServiceCollection c /// 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); + return TryAddKeyedAWSService(collection, serviceKey, options: null, lifetime); } /// @@ -228,12 +362,45 @@ public static IServiceCollection TryAddKeyedAWSService(this IServiceCollectio /// 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, (sp, _) => CreateServiceClient(options, sp), lifetime); + collection.TryAdd(descriptor); + return collection; + } - var descriptor = new ServiceDescriptor(typeof(T), serviceKey, Factory, 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. + /// A func that resolves 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, Func optionsFunc, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService + { + var descriptor = new ServiceDescriptor(typeof(T), serviceKey, (sp, _) => CreateServiceClient(optionsFunc(sp), sp), lifetime); collection.TryAdd(descriptor); return collection; } #endif + + private static object CreateServiceClient(AWSOptions options, IServiceProvider sp) where T : IAmazonService + { + var logger = sp.GetService(); + var awsOptions = options ?? sp.GetService() ?? new AWSOptions(); + var credentialsFactory = sp.GetService() ?? sp.CreateDefaultAWSCredentials(awsOptions); + + var factory = new ClientFactory(awsOptions, credentialsFactory, logger); + + return factory.CreateServiceClient(); + } + + private static AWSCredentials CreateDefaultAWSCredentials(this IServiceProvider sp, AWSOptions options = null) + { + options = options ?? sp.GetService() ?? new AWSOptions(); + return new DefaultAWSCredentials(options, sp.GetService()); + } } } diff --git a/extensions/test/NETCore.SetupTests/ConfigurationTests.cs b/extensions/test/NETCore.SetupTests/ConfigurationTests.cs index 731e5f2edefb..cf7aeaa97d25 100644 --- a/extensions/test/NETCore.SetupTests/ConfigurationTests.cs +++ b/extensions/test/NETCore.SetupTests/ConfigurationTests.cs @@ -10,6 +10,7 @@ using Amazon; using Amazon.S3; using Amazon.Runtime; +using AWSSDK.Extensions.NETCore.Setup; namespace NETCore.SetupTests {