-
Notifications
You must be signed in to change notification settings - Fork 865
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reuse default AWSOptions
in client-specific config
#3754
Comments
Here's a possible implementation of part of what I propose. There are two big issues with this as-is:
using Amazon.Extensions.NETCore.Setup;
using Amazon.Runtime;
using Microsoft.Extensions.DependencyInjection;
namespace AWSSDK.Extensions.NETCore.Setup.ServiceConfig;
public static class AdditionalServiceCollectionExtensions
{
// this should really be added to the lib's existing `ServiceCollectionExtensions.cs` class and reworked to use the internal `ClientFactory`
public static IServiceCollection AddAWSService<TService>(
this IServiceCollection services,
Func<IServiceProvider, AWSOptions> optionsFunc,
ServiceLifetime lifetime = ServiceLifetime.Singleton)
where TService : IAmazonService
{
var descriptor = new ServiceDescriptor(
typeof(TService),
sp =>
{
var userOptions = optionsFunc(sp);
return userOptions.CreateServiceClient<TService>();
},
lifetime);
services.Add(descriptor);
return services;
}
}
using Amazon.Extensions.NETCore.Setup;
using Amazon.Runtime;
using Microsoft.Extensions.DependencyInjection;
namespace AWSSDK.Extensions.NETCore.Setup.ServiceConfig;
public static class ClientConfigServiceProviderExtensions
{
public static AWSOptions GetAWSOptions<TConfig>(this IServiceProvider sp)
where TConfig : ClientConfig, new()
{
return sp.GetRequiredKeyedService<AWSOptions>(typeof(TConfig));
}
}
using Amazon.Extensions.NETCore.Setup;
using Amazon.Runtime;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace AWSSDK.Extensions.NETCore.Setup.ServiceConfig;
public static class ClientConfigServiceCollectionExtensions
{
public static IServiceCollection AddAWSOptions<TConfig>(
this IServiceCollection services,
Func<IServiceProvider, AWSOptions, TConfig, AWSOptions> optionsFunc = null,
ServiceLifetime lifetime = ServiceLifetime.Singleton)
where TConfig : ClientConfig, new()
{
var serviceDescriptor = new ServiceDescriptor(
serviceType: typeof(AWSOptions),
serviceKey: typeof(TConfig),
(sp, _) => sp.GetOptionsViaFunc(optionsFunc),
lifetime);
services.Add(serviceDescriptor);
return services;
}
public static IServiceCollection AddAWSService<TService, TConfig>(
this IServiceCollection services,
ServiceLifetime lifetime = ServiceLifetime.Singleton)
where TService : IAmazonService
where TConfig : ClientConfig, new()
{
return services.AddAWSService<TService>(
sp =>
{
var options = sp.GetAWSOptions<TConfig>();
return options;
},
lifetime);
}
public static IServiceCollection AddAWSService<TService, TConfig>(
this IServiceCollection services,
Func<IServiceProvider, AWSOptions, TConfig, AWSOptions> optionsFunc,
ServiceLifetime lifetime = ServiceLifetime.Singleton)
where TService : IAmazonService
where TConfig : ClientConfig, new()
{
return services.AddAWSService<TService>(
sp => sp.GetOptionsViaFunc(optionsFunc),
lifetime);
}
public static IServiceCollection AddAWSService<TService, TConfig>(
this IServiceCollection services,
Action<IServiceProvider, TConfig> configModifier,
ServiceLifetime lifetime = ServiceLifetime.Singleton)
where TService : IAmazonService
where TConfig : ClientConfig, new()
{
return services.AddAWSService<TService, TConfig>(
(sp, options, config) =>
{
configModifier(sp, config);
return options;
},
lifetime);
}
private static AWSOptions GetOptionsViaFunc<TConfig>(this IServiceProvider sp, Func<IServiceProvider, AWSOptions, TConfig, AWSOptions> optionsFunc)
where TConfig : ClientConfig, new()
{
var configuration = sp.GetRequiredService<IConfiguration>();
var defaultOptions = sp.GetService<AWSOptions>() ?? configuration.GetAWSOptions();
var newOptions = configuration.GetAWSOptions<TConfig>();
// this is really unfortunate, but I don't know of another way to recognize the globally-registered AWSOptions while creating service-specific config.
// See https://github.com/aws/aws-sdk-net/issues/3754
newOptions.ApplyPropsFrom(defaultOptions);
var clientConfig = (TConfig)newOptions.DefaultClientConfig;
return optionsFunc(sp, newOptions, clientConfig);
}
}
using Amazon.Extensions.NETCore.Setup;
using Amazon.Runtime;
namespace AWSSDK.Extensions.NETCore.Setup.ServiceConfig;
public static class AWSOptionsExtensions
{
/// <remarks>
/// https://github.com/aws/aws-sdk-net/issues/3754
/// </remarks>
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);
}
/// <remarks>
/// https://github.com/aws/aws-sdk-net/issues/3754
/// </remarks>
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.HasValue)
{
config.Timeout = other.Timeout.Value;
}
}
} |
Hi @chase-miller. Just giving you quick feedback before I go on vacation for a week, possibly somebody else on the team will chime in while I'm gone. I haven't thought through the implementation but a version of your option 1 is my preferred user experience where a user can provide an |
@normj thanks for the feedback and have a great vacation! |
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
orServiceURL
) 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.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.
2.
Expose an extension method that provides a (new)
AWSOptions
object created using the default AWSOptions if one is registered.3.
Expose a method/constructor that creates an
AWSOptions
object from another. Note that this example also updatesAWSOptions
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 newAddAWSService
extension method that takes aFunc<IServiceProvider, AWSOptions>
arg.Other Information
The most reasonable approach I could muster is to create an ugly
ApplyPropsFrom
extension method.Acknowledgements
AWS .NET SDK and/or Package version used
Targeted .NET Platform
.NET 8
Operating System and version
macOS
The text was updated successfully, but these errors were encountered: