aws / aws/aws-sdk-net

Reuse default `AWSOptions` in client-specific config

Open
#3,754 5 comments 1 reaction 0 assignees View on GitHub
Extensions feature-request p2 queued
Dominant language
C#
Stars
140
Forks
891
Avg merge
21h 51m
Merged PRs (30d)
10

Description

### 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.
```cs
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() // sqs will use the default "global" options added above
.AddSingleton(sp =>
{
// The only way to create an AWSOptions object with an AmazonS3Config DefaultClientConfig prop is via this extension method.
var awsOptions = sp.GetRequiredService().GetAWSOptions();

// 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();
});

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.
```cs
builder.Services
.AddDefaultAWSOptions(sp => sp.GetRequiredService().GetAWSOptions())
.AddAWSService()
.AddAWSService((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.
```cs
builder.Services
.AddDefaultAWSOptions(sp => sp.GetRequiredService().GetAWSOptions())
.AddAWSService()
.AddAWSService((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` arg.
```cs
builder.Services
.AddDefaultAWSOptions(sp => sp.GetRequiredService().GetAWSOptions())
.AddAWSService()
.AddAWSService(sp =>
{
var awsOptions = sp.GetRequiredService();
var sqsOptions = new AWSOptions(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.

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

var builder = WebApplication.CreateBuilder(args);

builder.Services
.AddDefaultAWSOptions(sp => sp.GetRequiredService().GetAWSOptions())
.AddAWSService()
.AddSingleton(sp =>
{
var globalAwsOptions = sp.GetRequiredService();
var config = sp.GetRequiredService();

var s3Options = config.GetAWSOptions();

s3Options.ApplyPropsFrom(globalAwsOptions);

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

return s3Options.CreateServiceClient();
});

var app = builder.Build();

app.Run();
```

```cs
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

- [x] I may be able to implement this feature request
- [ ] This feature might incur a breaking change

### AWS .NET SDK and/or Package version used







### Targeted .NET Platform

.NET 8

### Operating System and version

macOS

Contributor guide

Open the contributing guide

Research direction

Read the existing AddDefaultAWSOptions, AddAWSService, AWSOptions, and GetAWSOptions entry points to understand how default and service-specific configuration are currently created. Compare the proposed reuse approaches and existing registration behavior; done means client-specific configuration can inherit default AWSOptions without repeating shared settings.

Written by the indexing model from the issue text.

Assessment

Tech stack
aws, csharp
Domain
backend, developer-experience
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.