Skip to content

Reuse default AWSOptions in client-specific config #3754

Open
@chase-miller

Description

@chase-miller

Describe the feature

Expose the ability to register a service-client with service-specific config that reuses the default AWSOptions.

Use Case

I'd like to register an aws service with client-specific config overrides that uses the default AWSOptions. Without the ability to do this, I would have to duplicate option declarations (e.g. Credentials or ServiceURL) for each service-specific config I need to setup.

This is particularly important in scenarios where registrations can be modified outside of startup (e.g. WebApplicationFactory).

Ultimately I want my DI registrations to be the source of truth for how the AWS SDK behaves, and I want these registrations to be clean and DRY.

Here's a contrived example. An explicit AWSOptions instantiation is used for emphasis.

using Amazon.Extensions.NETCore.Setup;
using Amazon.Runtime;
using Amazon.S3;
using Amazon.SQS;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddDefaultAWSOptions(new AWSOptions
    {
        Credentials = new BasicAWSCredentials("accessKey", "accessSecret"),
        DefaultClientConfig =
        {
            ServiceURL = "localhost:6379",
        },
    })
    .AddAWSService<IAmazonSQS>() // sqs will use the default "global" options added above
    .AddSingleton<IAmazonS3>(sp =>
    {
        // The only way to create an AWSOptions object with an AmazonS3Config DefaultClientConfig prop is via this extension method.
        var awsOptions = sp.GetRequiredService<IConfiguration>().GetAWSOptions<AmazonSQSConfig>();

        // We have to declare these options again.
        awsOptions.Credentials = new BasicAWSCredentials("accessKey", "accessSecret");
        awsOptions.DefaultClientConfig.ServiceURL = "localhost:6379";

        var s3Config = (AmazonS3Config)awsOptions.DefaultClientConfig;
        s3Config.ForcePathStyle = true;
        s3Config.MaxErrorRetry = 10;
        // Set any other s3-specific config here

        return awsOptions.CreateServiceClient<IAmazonS3>();
    });

var app = builder.Build();

app.Run();

Proposed Solution

There are a number of approaches that could be taken. Here are some ideas in order of my personal preference (although I'd do 1 & 2 together):

1.

Expose an extension method that allows for service-specific config customization.

builder.Services
    .AddDefaultAWSOptions(sp => sp.GetRequiredService<IConfiguration>().GetAWSOptions())
    .AddAWSService<IAmazonSQS>()
    .AddAWSService<IAmazonS3, AmazonSQSConfig>((IServiceProvider sp, AmazonS3Config s3Config) =>
    {
        s3Config.ForcePathStyle = true;
        s3Config.MaxErrorRetry = 10;

        return s3Config;
    });

2.

Expose an extension method that provides a (new) AWSOptions object created using the default AWSOptions if one is registered.

builder.Services
    .AddDefaultAWSOptions(sp => sp.GetRequiredService<IConfiguration>().GetAWSOptions())
    .AddAWSService<IAmazonSQS>()
    .AddAWSService<IAmazonS3, AmazonS3Config>((IServiceProvider sp, AWSOptions awsOptions) =>
    {
        var sqsOptions = (AmazonS3Config)awsOptions.DefaultClientConfig;

        sqsOptions.ForcePathStyle = true;
        sqsOptions.MaxErrorRetry = 10;

        return awsOptions;
    })

3.

Expose a method/constructor that creates an AWSOptions object from another. Note that this example also updates AWSOptions to take a generic for ease of use, but that's not strictly necessary as long as there's some means of creating one outside of the IConfiguration extension method. Note that this also exposes a new AddAWSService extension method that takes a Func<IServiceProvider, AWSOptions> arg.

builder.Services
    .AddDefaultAWSOptions(sp => sp.GetRequiredService<IConfiguration>().GetAWSOptions())
    .AddAWSService<IAmazonSQS>()
    .AddAWSService<IAmazonS3>(sp =>
    {
        var awsOptions = sp.GetRequiredService<AWSOptions>();
        var sqsOptions = new AWSOptions<AmazonS3Config>(awsOptions);

        sqsOptions.DefaultClientConfig.ForcePathStyle = true;
        sqsOptions.DefaultClientConfig.MaxErrorRetry = 10;

        return sqsOptions;
    })

Other Information

The most reasonable approach I could muster is to create an ugly ApplyPropsFrom extension method.

using Amazon.Extensions.NETCore.Setup;
using Amazon.S3;
using Amazon.SQS;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddDefaultAWSOptions(sp => sp.GetRequiredService<IConfiguration>().GetAWSOptions())
    .AddAWSService<IAmazonSQS>()
    .AddSingleton<IAmazonS3>(sp =>
    {
        var globalAwsOptions = sp.GetRequiredService<AWSOptions>();
        var config = sp.GetRequiredService<IConfiguration>();

        var s3Options = config.GetAWSOptions<AmazonS3Config>();

        s3Options.ApplyPropsFrom(globalAwsOptions);

        var s3Config = (AmazonS3Config)s3Options.DefaultClientConfig;
        s3Config.ForcePathStyle = true;
        s3Config.MaxErrorRetry = 10;
        // Set any other s3-specific config here

        return s3Options.CreateServiceClient<IAmazonS3>();
    });

var app = builder.Build();

app.Run();
using Amazon.Extensions.NETCore.Setup;
using Amazon.Runtime;

namespace MyApp;

public static class AWSOptionsExtensions
{
    public static void ApplyPropsFrom(this AWSOptions options, AWSOptions? other)
    {
        if (other == null)
        {
            return;
        }

        // Hope and pray that AWSOptions props never change...
        options.Credentials = other.Credentials;
        options.Region = other.Region;
        options.Logging = other.Logging;
        options.Profile = other.Profile;
        options.ExternalId = other.ExternalId;
        options.ProfilesLocation = other.ProfilesLocation;
        options.SessionName = other.SessionName;
        options.DefaultConfigurationMode = other.DefaultConfigurationMode;
        options.SessionRoleArn = other.SessionRoleArn;

        options.DefaultClientConfig?.ApplyPropsFrom(other.DefaultClientConfig);
    }

    public static void ApplyPropsFrom(this ClientConfig config, ClientConfig? other)
    {
        if (other == null)
        {
            return;
        }

        // Hope and pray that ClientConfig props never change...
        config.ServiceId = other.ServiceId;
        config.DefaultConfigurationMode = other.DefaultConfigurationMode;
        config.RegionEndpoint = other.RegionEndpoint;
        config.ThrottleRetries = other.ThrottleRetries;
        config.UseHttp = other.UseHttp;
        config.UseAlternateUserAgentHeader = other.UseAlternateUserAgentHeader;
        config.ServiceURL = other.ServiceURL;
        config.SignatureVersion = other.SignatureVersion;
        config.ClientAppId = other.ClientAppId;
        config.SignatureMethod = other.SignatureMethod;
        config.LogResponse = other.LogResponse;
        config.BufferSize = other.BufferSize;
        config.ProgressUpdateInterval = other.ProgressUpdateInterval;
        config.ResignRetries = other.ResignRetries;
        config.ProxyCredentials = other.ProxyCredentials;
        config.LogMetrics = other.LogMetrics;
        config.DisableLogging = other.DisableLogging;
        config.AllowAutoRedirect = other.AllowAutoRedirect;
        config.UseDualstackEndpoint = other.UseDualstackEndpoint;
        config.UseFIPSEndpoint = other.UseFIPSEndpoint;
        config.DisableRequestCompression = other.DisableRequestCompression;
        config.RequestMinCompressionSizeBytes = other.RequestMinCompressionSizeBytes;
        config.DisableHostPrefixInjection = other.DisableHostPrefixInjection;
        config.EndpointDiscoveryEnabled = other.EndpointDiscoveryEnabled;
        config.IgnoreConfiguredEndpointUrls = other.IgnoreConfiguredEndpointUrls;
        config.EndpointDiscoveryCacheLimit = other.EndpointDiscoveryCacheLimit;
        config.RetryMode = other.RetryMode;
        config.TelemetryProvider = other.TelemetryProvider;
        config.AccountIdEndpointMode = other.AccountIdEndpointMode;
        config.RequestChecksumCalculation = other.RequestChecksumCalculation;
        config.ResponseChecksumValidation = other.ResponseChecksumValidation;
        config.ProxyHost = other.ProxyHost;
        config.ProxyPort = other.ProxyPort;
        config.Profile = other.Profile;
        config.AWSTokenProvider = other.AWSTokenProvider;
        config.AuthenticationRegion = other.AuthenticationRegion;
        config.AuthenticationServiceName = other.AuthenticationServiceName;
        config.MaxErrorRetry = other.MaxErrorRetry;
        config.FastFailRequests = other.FastFailRequests;
        config.CacheHttpClient = other.CacheHttpClient;
        config.HttpClientCacheSize = other.HttpClientCacheSize;
        config.EndpointProvider = other.EndpointProvider;
        config.MaxConnectionsPerServer = other.MaxConnectionsPerServer;
        config.HttpClientFactory = other.HttpClientFactory;

        if (other.Timeout.HasValue)
        {
            config.Timeout = other.Timeout.Value;
        }
    }
}

Acknowledgements

  • I may be able to implement this feature request
  • This feature might incur a breaking change

AWS .NET SDK and/or Package version used

<PackageVersion Include="AWSSDK.Core" Version="3.7.402.19" />
<PackageVersion Include="AWSSDK.S3" Version="3.7.201.1" />
<PackageVersion Include="AWSSDK.SQS" Version="3.7.400.102" />
<PackageVersion Include="AWSSDK.SSO" Version="3.7.400.113" />
<PackageVersion Include="AWSSDK.SSOOIDC" Version="3.7.400.114" />
<PackageVersion Include="AWSSDK.SecurityToken" Version="3.7.202.11" />
<PackageVersion Include="AWSSDK.SimpleEmail" Version="3.7.200.29" />

Targeted .NET Platform

.NET 8

Operating System and version

macOS

Metadata

Metadata

Assignees

No one assigned

    Labels

    Extensionsfeature-requestA feature should be added or improved.p2This is a standard priority issuequeued

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions