Skip to content
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

Add AoT support to NETCore.Setup #3174

Draft
wants to merge 1 commit into
base: main-staging
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -18,7 +18,7 @@
<Compile Remove="**/obj/**" />
</ItemGroup>

<PropertyGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
<WarningsAsErrors>IL2026,IL2075</WarningsAsErrors>
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>
Expand Down
32 changes: 21 additions & 11 deletions extensions/src/AWSSDK.Extensions.NETCore.Setup/AWSOptions.cs
Expand Up @@ -12,24 +12,14 @@
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using Amazon;
using System.Diagnostics.CodeAnalysis;
using Amazon.Runtime;

using Amazon.Extensions.NETCore.Setup;

namespace Amazon.Extensions.NETCore.Setup
{
/// <summary>
/// The options used to construct AWS service clients like the Amazon.S3.AmazonS3Client.
/// </summary>
#if NET8_0_OR_GREATER
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode(Amazon.Extensions.NETCore.Setup.InternalConstants.RequiresUnreferencedCodeMessage)]
#endif
public class AWSOptions
{
/// <summary>
Expand Down Expand Up @@ -103,11 +93,31 @@ internal set
/// </summary>
/// <typeparam name="T">The service interface that a service client will be created for.</typeparam>
/// <returns>The service client that implements the service interface.</returns>
#if NET8_0_OR_GREATER
[RequiresUnreferencedCode(InternalConstants.RequiresUnreferencedCodeMessage)]
#endif
public T CreateServiceClient<T>() where T : IAmazonService
{
return (T)ClientFactory.CreateServiceClient(null, typeof(T), this);
}

#if NET8_0_OR_GREATER
/// <summary>
/// Create a service client for the specified service using the specified options.
/// For example if T is set to AmazonS3 then the AmazonS3ServiceClient which implements IAmazonS3 is created
/// and returned.
/// </summary>
/// <typeparam name="TClient">The service interface that a service client will be created for.</typeparam>
/// <typeparam name="TConfiguration">The type of the service configuration the client will be created with.</typeparam>
/// <returns>The service client of the specified type.</returns>
public TClient CreateServiceClient<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TClient, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TConfiguration>()
where TClient : class, IAmazonService
where TConfiguration : ClientConfig, new()
{
return ClientFactory.CreateServiceClient<TClient, TConfiguration>(null, this);
}
#endif

/// <summary>
/// Container for logging settings of the SDK
/// </summary>
Expand Down
Expand Up @@ -14,10 +14,14 @@
<GenerateAssemblyFileVersionAttribute>false</GenerateAssemblyFileVersionAttribute>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
<WarningsAsErrors>IL2026,IL2075</WarningsAsErrors>
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>
<EnableAotAnalyzer>true</EnableAotAnalyzer>
<EnableSingleFileAnalyzer>true</EnableSingleFileAnalyzer>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>

<Choose>
<When Condition=" '$(AWSKeyFile)' == '' ">
Expand All @@ -37,6 +41,12 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.0.0" />
</ItemGroup>

<ItemGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
<PackageReference Update="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Update="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageReference Update="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="AWSSDK.Core" Version="3.7.300" />
Expand Down
162 changes: 157 additions & 5 deletions extensions/src/AWSSDK.Extensions.NETCore.Setup/ClientFactory.cs
Expand Up @@ -13,6 +13,7 @@
* permissions and limitations under the License.
*/
using System;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Amazon.Runtime;
using Amazon.Runtime.CredentialManagement;
Expand All @@ -26,9 +27,6 @@ namespace Amazon.Extensions.NETCore.Setup
/// <summary>
/// The factory class for creating AWS service clients from the AWS SDK for .NET.
/// </summary>
#if NET8_0_OR_GREATER
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode(Amazon.Extensions.NETCore.Setup.InternalConstants.RequiresUnreferencedCodeMessage)]
#endif
internal class ClientFactory
{
private static readonly Type[] EMPTY_TYPES = Array.Empty<Type>();
Expand All @@ -53,9 +51,12 @@ internal ClientFactory(Type type, AWSOptions awsOptions)
/// </summary>
/// <param name="provider">The dependency injection provider.</param>
/// <returns>The AWS service client</returns>
#if NET8_0_OR_GREATER
[RequiresUnreferencedCode(InternalConstants.RequiresUnreferencedCodeMessage)]
#endif
internal object CreateServiceClient(IServiceProvider provider)
{
var loggerFactory = provider.GetService<Microsoft.Extensions.Logging.ILoggerFactory>();
var loggerFactory = provider.GetService<ILoggerFactory>();
var logger = loggerFactory?.CreateLogger("AWSSDK");

var options = _awsOptions ?? provider.GetService<AWSOptions>();
Expand All @@ -79,6 +80,9 @@ internal object CreateServiceClient(IServiceProvider provider)
/// </summary>
/// <param name="provider">The dependency injection provider.</param>
/// <returns>The AWS service client</returns>
#if NET8_0_OR_GREATER
[RequiresUnreferencedCode(InternalConstants.RequiresUnreferencedCodeMessage)]
#endif
internal static IAmazonService CreateServiceClient(ILogger logger, Type serviceInterfaceType, AWSOptions options)
{
PerformGlobalConfig(logger, options);
Expand All @@ -94,6 +98,47 @@ internal static IAmazonService CreateServiceClient(ILogger logger, Type serviceI
return client as IAmazonService;
}

#if NET8_0_OR_GREATER
/// <summary>
/// Creates the AWS service client that implements the service client interface. The AWSOptions object
/// will be searched for in the IServiceProvider.
/// </summary>
/// <param name="provider">The dependency injection provider.</param>
/// <returns>The AWS service client</returns>
internal static TClient CreateServiceClient<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TClient, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TConfiguration>(
IServiceProvider provider,
AWSOptions options)
where TClient : class, IAmazonService
where TConfiguration : ClientConfig, new()
{
var loggerFactory = provider?.GetService<ILoggerFactory>();
var logger = loggerFactory?.CreateLogger("AWSSDK");

options ??= provider.GetService<AWSOptions>();
if (options == null)
{
var configuration = provider.GetService<IConfiguration>();
if (configuration != null)
{
options = configuration.GetAWSOptions();
if (options != null)
logger?.LogInformation("Found AWS options in IConfiguration");
}
}

PerformGlobalConfig(logger, options);
var credentials = CreateCredentials(logger, options);

if (!string.IsNullOrEmpty(options?.SessionRoleArn))
{
credentials = new AssumeRoleAWSCredentials(credentials, options.SessionRoleArn, options.SessionName);
}

var config = CreateConfig<TConfiguration>(options);
return CreateClient<TClient, TConfiguration>(credentials, config);
}
#endif

/// <summary>
/// Performs all of the global settings that have been specified in AWSOptions.
/// </summary>
Expand Down Expand Up @@ -135,6 +180,9 @@ private static void PerformGlobalConfig(ILogger logger, AWSOptions options)
/// <param name="credentials"></param>
/// <param name="config"></param>
/// <returns></returns>
#if NET8_0_OR_GREATER
[RequiresUnreferencedCode(InternalConstants.RequiresUnreferencedCodeMessage)]
#endif
private static AmazonServiceClient CreateClient(Type serviceInterfaceType, AWSCredentials credentials, ClientConfig config)
{
var clientTypeName = serviceInterfaceType.Namespace + "." + serviceInterfaceType.Name.Substring(1) + "Client";
Expand All @@ -153,6 +201,40 @@ private static AmazonServiceClient CreateClient(Type serviceInterfaceType, AWSCr
return constructor.Invoke(new object[] { credentials, config }) as AmazonServiceClient;
}

#if NET8_0_OR_GREATER
/// <summary>
/// Creates the service client using the credentials and client config.
/// </summary>
/// <param name="credentials">The AWS credentials to use.</param>
/// <param name="config">The client configuration to use.</param>
/// <returns>
/// The created instance of <typeparamref name="TClient"/>TClient.
/// </returns>
private static TClient CreateClient<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TClient, TConfiguration>(
AWSCredentials credentials,
TConfiguration config)
where TClient : class, IAmazonService
where TConfiguration : ClientConfig, new()
{
if (typeof(TClient).IsInterface)
{
throw new AmazonClientException($"Service client {typeof(TClient)} is an interface and cannot be instantiated.");
}
else if (typeof(TClient).IsAbstract)
{
throw new AmazonClientException($"Service client {typeof(TClient)} is an abstract class and cannot be instantiated.");
}

var constructor = typeof(TClient).GetConstructor(new Type[] { typeof(AWSCredentials), typeof(TConfiguration) });
if (constructor == null)
{
throw new AmazonClientException($"Service client {typeof(TClient)} missing a constructor with parameters AWSCredentials and {typeof(TConfiguration).FullName}.");
}

return (TClient)constructor.Invoke(new object[] { credentials, config });
}
#endif

/// <summary>
/// Creates the AWSCredentials using either the profile indicated from the AWSOptions object
/// of the SDK fallback credentials search.
Expand Down Expand Up @@ -203,6 +285,9 @@ private static AWSCredentials CreateCredentials(ILogger logger, AWSOptions optio
/// </summary>
/// <param name="options"></param>
/// <returns></returns>
#if NET8_0_OR_GREATER
[RequiresUnreferencedCode(InternalConstants.RequiresUnreferencedCodeMessage)]
#endif
private static ClientConfig CreateConfig(Type serviceInterfaceType, AWSOptions options)
{
var configTypeName = serviceInterfaceType.Namespace + "." + serviceInterfaceType.Name.Substring(1) + "Config";
Expand All @@ -222,7 +307,7 @@ private static ClientConfig CreateConfig(Type serviceInterfaceType, AWSOptions o
}

var defaultConfig = options.DefaultClientConfig;
var emptyArray = new object[0];
var emptyArray = Array.Empty<object>();
var singleArray = new object[1];

var clientConfigTypeInfo = options.DefaultClientConfig.GetType();
Expand Down Expand Up @@ -265,5 +350,72 @@ private static ClientConfig CreateConfig(Type serviceInterfaceType, AWSOptions o

return config;
}

#if NET8_0_OR_GREATER
/// <summary>
/// Creates the ClientConfig object for the service client.
/// </summary>
/// <param name="options"></param>
/// <returns></returns>
private static TConfiguration CreateConfig<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TConfiguration>(AWSOptions options)
where TConfiguration : ClientConfig, new()
{
var config = new TConfiguration();

if (options == null)
{
options = new AWSOptions();
}

if (options.DefaultConfigurationMode.HasValue)
{
config.DefaultConfigurationMode = options.DefaultConfigurationMode.Value;
}

var defaultConfig = options.DefaultClientConfig;
var emptyArray = Array.Empty<object>();
var singleArray = new object[1];

var clientConfigTypeInfo = options.DefaultClientConfig.GetType();
var properties = clientConfigTypeInfo.GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var property in properties)
{
if (property.GetMethod != null && property.SetMethod != null)
{
// Skip RegionEndpoint because it is set below and calling the get method on the
// property triggers the default region fallback mechanism.
if (string.Equals(property.Name, "RegionEndpoint", StringComparison.Ordinal))
continue;

// DefaultConfigurationMode is skipped from the DefaultClientConfig because it is expected to be set
// at the top level of AWSOptions which is done before this loop.
if (string.Equals(property.Name, "DefaultConfigurationMode", StringComparison.Ordinal))
continue;

// Skip setting RetryMode if it is set to legacy but the DefaultConfigurationMode is not legacy.
// This will allow the retry mode to be configured from the DefaultConfiguration.
// This is a workaround to handle the inability to tell if RetryMode was explicitly set.
if (string.Equals(property.Name, "RetryMode", StringComparison.Ordinal) &&
defaultConfig.RetryMode == RequestRetryMode.Legacy &&
config.DefaultConfigurationMode != DefaultConfigurationMode.Legacy)
continue;

singleArray[0] = property.GetMethod.Invoke(defaultConfig, emptyArray);
if (singleArray[0] != null)
{
property.SetMethod.Invoke(config, singleArray);
}
}
}

// Setting RegionEndpoint only if ServiceURL was not set, because ServiceURL value will be lost otherwise
if (options.Region != null && string.IsNullOrEmpty(defaultConfig.ServiceURL))
{
config.RegionEndpoint = options.Region;
}

return config;
}
#endif
}
}
Expand Up @@ -21,16 +21,14 @@

using Amazon.Extensions.NETCore.Setup;
using System.Linq;
using System.Diagnostics.CodeAnalysis;

namespace Microsoft.Extensions.Configuration
{
/// <summary>
/// This class adds extension methods to IConfiguration making it easier to pull out
/// AWS configuration options.
/// </summary>
#if NET8_0_OR_GREATER
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode(Amazon.Extensions.NETCore.Setup.InternalConstants.RequiresUnreferencedCodeMessage)]
#endif
public static class ConfigurationExtensions
{
/// <summary>
Expand Down Expand Up @@ -65,7 +63,11 @@ public static AWSOptions GetAWSOptions(this IConfiguration config, string config
/// <typeparam name="TConfig">The AWS client config to be used in creating clients, like AmazonS3Config.</typeparam>
/// <param name="config"></param>
/// <returns>The AWSOptions containing the values set in configuration system.</returns>
#if NET8_0_OR_GREATER
public static AWSOptions GetAWSOptions<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] TConfig>(this IConfiguration config) where TConfig : ClientConfig, new()
#else
public static AWSOptions GetAWSOptions<TConfig>(this IConfiguration config) where TConfig : ClientConfig, new()
#endif
{
return GetAWSOptions<TConfig>(config, DEFAULT_CONFIG_SECTION);
}
Expand All @@ -77,7 +79,11 @@ public static AWSOptions GetAWSOptions<TConfig>(this IConfiguration config) wher
/// <param name="config"></param>
/// <param name="configSection">The config section to extract AWS options from.</param>
/// <returns>The AWSOptions containing the values set in configuration system.</returns>
#if NET8_0_OR_GREATER
public static AWSOptions GetAWSOptions<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] TConfig>(this IConfiguration config, string configSection) where TConfig : ClientConfig, new()
#else
public static AWSOptions GetAWSOptions<TConfig>(this IConfiguration config, string configSection) where TConfig : ClientConfig, new()
#endif
{
var options = new AWSOptions
{
Expand Down
Expand Up @@ -17,6 +17,6 @@ namespace Amazon.Extensions.NETCore.Setup
{
internal static class InternalConstants
{
internal const string RequiresUnreferencedCodeMessage = "The AWSSDK.Extensions.NETCore.Setup package has not been updated to support Native AOT compilations.";
internal const string RequiresUnreferencedCodeMessage = "This method requires code to be dynamically loaded and is not supported in applications using Native AOT compilation.";
}
}