From 50cf24fd8c1f0b8533c482fb8b6a88647ed1fd39 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Thu, 26 Jun 2025 12:06:04 -0400 Subject: [PATCH 1/3] add native aot error message --- .../Custom/DataModel/ContextInternal.cs | 83 ++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs index 4beb34fd68cb..1e86e4516bff 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs @@ -629,6 +629,72 @@ private object FromDynamoDBEntry(SimplePropertyStorage propertyStorage, DynamoDB if (entry is DynamoDBNull) return null; + // Check for Native AOT collection type issues + if (InternalSDKUtils.IsRunningNativeAot() && targetType != null) + { + // Skip checking primitive types - they always work + if (!Utils.PrimitiveTypes.Contains(targetType) && + !Utils.PrimitiveTypesCollectionsAndArray.Contains(targetType) && + Utils.IsCollectionType(targetType)) + { + // Check if this is a generic collection + if (targetType.IsGenericType) + { + // Get the generic type definition + Type genericTypeDef = targetType.GetGenericTypeDefinition(); + + // Handle list-like collections + if ((entry is DynamoDBList || entry is PrimitiveList) && + (genericTypeDef == typeof(List<>) || + genericTypeDef == typeof(IList<>) || + genericTypeDef == typeof(HashSet<>) || + genericTypeDef == typeof(IEnumerable<>))) + { + // Check if we can instantiate the collection type + if (!Utils.CanInstantiate(targetType)) + { + ThrowNativeAotTypeInstantiationError(targetType, propertyStorage.PropertyName, entry); + } + + // Get element type through Utils to maintain correct annotations + Type elementType = Utils.GetElementType(targetType); + + // Check if the element type can be instantiated + if (elementType != null && !Utils.CanInstantiate(elementType)) + { + ThrowNativeAotTypeInstantiationError(elementType, propertyStorage.PropertyName, entry); + } + } + // Handle dictionary-like collections + else if (entry is Document && + (genericTypeDef == typeof(Dictionary<,>) || + genericTypeDef == typeof(IDictionary<,>))) + { + // Check if we can instantiate the dictionary type + if (!Utils.CanInstantiate(targetType)) + { + ThrowNativeAotTypeInstantiationError(targetType, propertyStorage.PropertyName, entry); + } + + // Get key and value types directly with proper annotations + Type keyType = null, valueType = null; + IsSupportedDictionaryType(targetType, out keyType, out valueType); + + // Check if key and value types can be instantiated + if (keyType != null && !Utils.CanInstantiate(keyType)) + { + ThrowNativeAotTypeInstantiationError(keyType, propertyStorage.PropertyName, entry); + } + + if (valueType != null && !Utils.CanInstantiate(valueType)) + { + ThrowNativeAotTypeInstantiationError(valueType, propertyStorage.PropertyName, entry); + } + } + } + } + } + object output; Document document = entry as Document; if (document != null) @@ -729,8 +795,21 @@ private bool TryFromListToArray([DynamicallyAccessedMembers(InternalConstants.Da return true; } - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2067", - Justification = "The user's type has been annotated with InternalConstants.DataModelModeledType with the public API into the library. At this point the type will not be trimmed.")] + + + /// + /// Throws a detailed error message for Native AOT type instantiation issues + /// + private void ThrowNativeAotTypeInstantiationError(Type problematicType, string propertyName, DynamoDBEntry entry) + { + string errorMessage = $"Type {problematicType.FullName} is unsupported, it cannot be instantiated. Since the application is running in Native AOT mode the type could possibly be trimmed. " + + "This can happen if the type being created is a nested type of a type being used for saving and loading DynamoDB items. " + + $"This can be worked around by adding the \"[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof({problematicType.FullName}))]\" attribute to the constructor of the parent type." + + "If the parent type can not be modified the attribute can also be used on the method invoking the DynamoDB sdk or some other method that you are sure is not being trimmed."; + + throw new InvalidOperationException(errorMessage); + } + private bool TryFromMap([DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type targetType, Document map, DynamoDBFlatConfig flatConfig, SimplePropertyStorage parentPropertyStorage, out object output) { output = null; From d2b2c013761b4f9dc508152c2484dec9ab304715 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Thu, 26 Jun 2025 13:18:22 -0400 Subject: [PATCH 2/3] update --- .../DynamoDBv2/Custom/DataModel/ContextInternal.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs index 1e86e4516bff..8f00bcdc40b8 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs @@ -680,13 +680,17 @@ private object FromDynamoDBEntry(SimplePropertyStorage propertyStorage, DynamoDB Type keyType = null, valueType = null; IsSupportedDictionaryType(targetType, out keyType, out valueType); - // Check if key and value types can be instantiated - if (keyType != null && !Utils.CanInstantiate(keyType)) + // Check if key and value types can be instantiated (skip check for primitive types) + if (keyType != null && + !Utils.PrimitiveTypes.Contains(keyType) && + !Utils.CanInstantiate(keyType)) { ThrowNativeAotTypeInstantiationError(keyType, propertyStorage.PropertyName, entry); } - if (valueType != null && !Utils.CanInstantiate(valueType)) + if (valueType != null && + !Utils.PrimitiveTypes.Contains(valueType) && + !Utils.CanInstantiate(valueType)) { ThrowNativeAotTypeInstantiationError(valueType, propertyStorage.PropertyName, entry); } @@ -810,6 +814,8 @@ private void ThrowNativeAotTypeInstantiationError(Type problematicType, string p throw new InvalidOperationException(errorMessage); } + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2067", + Justification = "The user's type has been annotated with InternalConstants.DataModelModeledType with the public API into the library. At this point the type will not be trimmed.")] private bool TryFromMap([DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type targetType, Document map, DynamoDBFlatConfig flatConfig, SimplePropertyStorage parentPropertyStorage, out object output) { output = null; From df8ee26561367795d9ca8a9c171a6cb60fdefd67 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Fri, 27 Jun 2025 14:01:52 -0400 Subject: [PATCH 3/3] add test apps --- TestAotDeserialization_NoFix/Program.cs | 250 ++++++++++++++++++ .../TestAotDeserialization_NoFix.csproj | 23 ++ TestAotDeserialization_WithFix/Program.cs | 248 +++++++++++++++++ .../TestAotDeserialization_WithFix.csproj | 17 ++ 4 files changed, 538 insertions(+) create mode 100644 TestAotDeserialization_NoFix/Program.cs create mode 100644 TestAotDeserialization_NoFix/TestAotDeserialization_NoFix.csproj create mode 100644 TestAotDeserialization_WithFix/Program.cs create mode 100644 TestAotDeserialization_WithFix/TestAotDeserialization_WithFix.csproj diff --git a/TestAotDeserialization_NoFix/Program.cs b/TestAotDeserialization_NoFix/Program.cs new file mode 100644 index 000000000000..1c5e2119c08c --- /dev/null +++ b/TestAotDeserialization_NoFix/Program.cs @@ -0,0 +1,250 @@ +using System.Diagnostics.CodeAnalysis; +using Amazon; +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.DataModel; +using Amazon.DynamoDBv2.Model; +using System.Net; + +namespace TestAotDeserialization_NoFix; + +// This version intentionally does NOT include DynamicDependency attributes +// It should fail when deserializing List in Native AOT +[DynamoDBTable("TestQuotes")] +public class QuoteResponse +{ + // No DynamicDependency attributes here - this is what we're testing! + + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CarQuoteResponse))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(List))] + // [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CarQuoteExcess))] + public QuoteResponse() + { + } + + [DynamoDBHashKey] + public string Id { get; init; } = string.Empty; + + [DynamoDBRangeKey] + public string Insurer { get; init; } = string.Empty; + + [DynamoDBProperty("Live")] + public long LiveTime { get; init; } + + [DynamoDBProperty("CarQuoteResponse")] + public CarQuoteResponse CarQuoteResponse { get; init; } = new(); +} + +public class CarQuoteResponse +{ + public string Insurer { get; set; } = string.Empty; + public bool Successful { get; set; } + public long ElapsedMs { get; set; } + public decimal? TotalAnnual { get; set; } + public string QuoteRef { get; set; } = string.Empty; + public string QuoteRefUrl { get; set; } = string.Empty; + public bool CanEmail { get; set; } + public bool EmailSent { get; set; } + public string? AddressText { get; set; } + public string? VehicleDescription { get; set; } + public List Excesses { get; set; } = new List(); + public string FailureReason { get; set; } = string.Empty; + public bool CanDisplayFailure { get; set; } +} + +public class CarQuoteExcess +{ + public string Description { get; set; } = string.Empty; + public decimal? Excess { get; set; } +} + +class Program +{ + private static readonly RegionEndpoint region = RegionEndpoint.USWest2; + private const string TableName = "TestQuotes"; + + static async Task Main(string[] args) + { + try + { + Console.WriteLine("Starting DynamoDB Native AOT Test WITHOUT DynamicDependency Fix"); + Console.WriteLine("======================================================"); + + // Initialize DynamoDB client + var config = new AmazonDynamoDBConfig + { + RegionEndpoint = region + }; + + var client = new AmazonDynamoDBClient(config); + var context = new DynamoDBContext(client); + + // Step 1: Create table if it doesn't exist + await EnsureTableExists(client, TableName); + + // Step 2: Insert test data + string id = "test-id-" + DateTime.UtcNow.Ticks; + await InsertTestData(context, id); + + // Step 3: Query data and attempt to deserialize + Console.WriteLine("\nAttempting to query and deserialize data:"); + try + { + await QueryData(context, id); + Console.WriteLine("SUCCESS: No exception was thrown! The issue may have been fixed in the SDK."); + } + catch (Exception ex) + { + Console.WriteLine($"FAILURE: Exception occurred: {ex.Message}"); + Console.WriteLine($"Exception type: {ex.GetType().FullName}"); + Console.WriteLine($"Stack trace: {ex.StackTrace}"); + Console.WriteLine("\nThis confirms the issue exists without DynamicDependency attributes."); + } + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred: {ex.Message}"); + Console.WriteLine($"Stack trace: {ex.StackTrace}"); + } + + Console.WriteLine("\nTest completed. Press any key to exit."); + Console.ReadKey(); + } + + static async Task EnsureTableExists(IAmazonDynamoDB client, string tableName) + { + Console.WriteLine($"Checking if table '{tableName}' exists..."); + + try + { + var tableDescription = await client.DescribeTableAsync(tableName); + Console.WriteLine($"Table '{tableName}' already exists"); + } + catch (ResourceNotFoundException) + { + Console.WriteLine($"Table '{tableName}' does not exist, creating..."); + + var request = new CreateTableRequest + { + TableName = tableName, + AttributeDefinitions = new List + { + new AttributeDefinition { AttributeName = "Id", AttributeType = "S" }, + new AttributeDefinition { AttributeName = "Insurer", AttributeType = "S" } + }, + KeySchema = new List + { + new KeySchemaElement { AttributeName = "Id", KeyType = "HASH" }, + new KeySchemaElement { AttributeName = "Insurer", KeyType = "RANGE" } + }, + ProvisionedThroughput = new ProvisionedThroughput + { + ReadCapacityUnits = 5, + WriteCapacityUnits = 5 + } + }; + + var response = await client.CreateTableAsync(request); + Console.WriteLine($"Table '{tableName}' created successfully, waiting for it to be active..."); + + bool isTableActive = false; + while (!isTableActive) + { + var tableStatus = await client.DescribeTableAsync(tableName); + isTableActive = tableStatus.Table.TableStatus == TableStatus.ACTIVE; + if (!isTableActive) + { + Console.WriteLine("Table is being created, waiting 5 seconds..."); + await Task.Delay(5000); + } + } + + Console.WriteLine($"Table '{tableName}' is now active"); + } + } + + static async Task InsertTestData(DynamoDBContext context, string id) + { + Console.WriteLine($"Inserting test data..."); + + var excesses = new List + { + new CarQuoteExcess { Description = "Standard Excess", Excess = 250.00m }, + new CarQuoteExcess { Description = "Voluntary Excess", Excess = 100.00m }, + new CarQuoteExcess { Description = "Young Driver Excess", Excess = 300.00m } + }; + + var carQuoteResponse = new CarQuoteResponse + { + Insurer = "TestInsurer", + Successful = true, + ElapsedMs = 1500, + TotalAnnual = 450.75m, + QuoteRef = "Q12345", + QuoteRefUrl = "https://example.com/quote/Q12345", + CanEmail = true, + EmailSent = false, + AddressText = "123 Test Street, Test City", + VehicleDescription = "2022 Test Car Model", + Excesses = excesses, + FailureReason = string.Empty, + CanDisplayFailure = false + }; + + var quoteResponse = new QuoteResponse + { + Id = id, + Insurer = "TestInsurer", + LiveTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 86400, // 24 hours from now + CarQuoteResponse = carQuoteResponse + }; + + // Override the table name + var config = new DynamoDBOperationConfig { OverrideTableName = TableName }; + await context.SaveAsync(quoteResponse, config); + + Console.WriteLine("Test data inserted successfully"); + } + + static async Task QueryData(DynamoDBContext context, string id) + { + Console.WriteLine($"Querying data with Id '{id}'..."); + + var config = new DynamoDBOperationConfig + { + OverrideTableName = TableName, + ConsistentRead = true + }; + + try + { + var results = await context.QueryAsync(id, config).GetRemainingAsync(); + + Console.WriteLine($"Successfully retrieved {results.Count} items"); + + if (results.Count > 0) + { + Console.WriteLine("Data successfully deserialized!"); + Console.WriteLine("Accessing the nested List..."); + + // Access the nested list - this is where we expect failure without DynamicDependency + var excesses = results[0].CarQuoteResponse.Excesses; + Console.WriteLine($"Excesses list contains {excesses.Count} items"); + + // Print each excess to prove it's working + foreach (var excess in excesses) + { + Console.WriteLine($" * {excess.Description}: {excess.Excess:C}"); + } + } + else + { + Console.WriteLine("No items found with the given ID."); + } + } + catch (Exception) + { + Console.WriteLine("Failed to deserialize the List property"); + throw; // Re-throw to be caught by the outer try/catch + } + } +} diff --git a/TestAotDeserialization_NoFix/TestAotDeserialization_NoFix.csproj b/TestAotDeserialization_NoFix/TestAotDeserialization_NoFix.csproj new file mode 100644 index 000000000000..6f927cb48494 --- /dev/null +++ b/TestAotDeserialization_NoFix/TestAotDeserialization_NoFix.csproj @@ -0,0 +1,23 @@ + + + + Exe + net8.0 + enable + enable + true + true + + + + + + + + + + + diff --git a/TestAotDeserialization_WithFix/Program.cs b/TestAotDeserialization_WithFix/Program.cs new file mode 100644 index 000000000000..837c303de00c --- /dev/null +++ b/TestAotDeserialization_WithFix/Program.cs @@ -0,0 +1,248 @@ +using System.Diagnostics.CodeAnalysis; +using Amazon; +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.DataModel; +using Amazon.DynamoDBv2.Model; +using System.Net; + +namespace TestAotDeserialization_WithFix; + +// This version INCLUDES DynamicDependency attributes to fix the Native AOT trimming issue +[DynamoDBTable("TestQuotesWithFix")] +public class QuoteResponse +{ + // Include DynamicDependency attributes to prevent trimming - THIS IS THE FIX! + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CarQuoteResponse))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(List))] + // [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CarQuoteExcess))] + public QuoteResponse() + { + } + + [DynamoDBHashKey] + public string Id { get; init; } = string.Empty; + + [DynamoDBRangeKey] + public string Insurer { get; init; } = string.Empty; + + [DynamoDBProperty("Live")] + public long LiveTime { get; init; } + + [DynamoDBProperty("CarQuoteResponse")] + public CarQuoteResponse CarQuoteResponse { get; init; } = new(); +} + +public class CarQuoteResponse +{ + public string Insurer { get; set; } = string.Empty; + public bool Successful { get; set; } + public long ElapsedMs { get; set; } + public decimal? TotalAnnual { get; set; } + public string QuoteRef { get; set; } = string.Empty; + public string QuoteRefUrl { get; set; } = string.Empty; + public bool CanEmail { get; set; } + public bool EmailSent { get; set; } + public string? AddressText { get; set; } + public string? VehicleDescription { get; set; } + public List Excesses { get; set; } = new List(); + public string FailureReason { get; set; } = string.Empty; + public bool CanDisplayFailure { get; set; } +} + +public class CarQuoteExcess +{ + public string Description { get; set; } = string.Empty; + public decimal? Excess { get; set; } +} + +class Program +{ + private static readonly RegionEndpoint region = RegionEndpoint.USWest2; + private const string TableName = "TestQuotesWithFix"; + + static async Task Main(string[] args) + { + try + { + Console.WriteLine("Starting DynamoDB Native AOT Test WITH DynamicDependency Fix"); + Console.WriteLine("====================================================="); + + // Initialize DynamoDB client + var config = new AmazonDynamoDBConfig + { + RegionEndpoint = region + }; + + var client = new AmazonDynamoDBClient(config); + var context = new DynamoDBContext(client); + + // Step 1: Create table if it doesn't exist + await EnsureTableExists(client, TableName); + + // Step 2: Insert test data + string id = "test-id-" + DateTime.UtcNow.Ticks; + await InsertTestData(context, id); + + // Step 3: Query data and attempt to deserialize + Console.WriteLine("\nAttempting to query and deserialize data:"); + try + { + await QueryData(context, id); + Console.WriteLine("SUCCESS: No exception was thrown! The DynamicDependency fix worked!"); + } + catch (Exception ex) + { + Console.WriteLine($"FAILURE: Exception occurred: {ex.Message}"); + Console.WriteLine($"Exception type: {ex.GetType().FullName}"); + Console.WriteLine($"Stack trace: {ex.StackTrace}"); + Console.WriteLine("\nEven with the DynamicDependency attributes, an error occurred."); + } + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred: {ex.Message}"); + Console.WriteLine($"Stack trace: {ex.StackTrace}"); + } + + Console.WriteLine("\nTest completed. Press any key to exit."); + Console.ReadKey(); + } + + static async Task EnsureTableExists(IAmazonDynamoDB client, string tableName) + { + Console.WriteLine($"Checking if table '{tableName}' exists..."); + + try + { + var tableDescription = await client.DescribeTableAsync(tableName); + Console.WriteLine($"Table '{tableName}' already exists"); + } + catch (ResourceNotFoundException) + { + Console.WriteLine($"Table '{tableName}' does not exist, creating..."); + + var request = new CreateTableRequest + { + TableName = tableName, + AttributeDefinitions = new List + { + new AttributeDefinition { AttributeName = "Id", AttributeType = "S" }, + new AttributeDefinition { AttributeName = "Insurer", AttributeType = "S" } + }, + KeySchema = new List + { + new KeySchemaElement { AttributeName = "Id", KeyType = "HASH" }, + new KeySchemaElement { AttributeName = "Insurer", KeyType = "RANGE" } + }, + ProvisionedThroughput = new ProvisionedThroughput + { + ReadCapacityUnits = 5, + WriteCapacityUnits = 5 + } + }; + + var response = await client.CreateTableAsync(request); + Console.WriteLine($"Table '{tableName}' created successfully, waiting for it to be active..."); + + bool isTableActive = false; + while (!isTableActive) + { + var tableStatus = await client.DescribeTableAsync(tableName); + isTableActive = tableStatus.Table.TableStatus == TableStatus.ACTIVE; + if (!isTableActive) + { + Console.WriteLine("Table is being created, waiting 5 seconds..."); + await Task.Delay(5000); + } + } + + Console.WriteLine($"Table '{tableName}' is now active"); + } + } + + static async Task InsertTestData(DynamoDBContext context, string id) + { + Console.WriteLine($"Inserting test data..."); + + var excesses = new List + { + new CarQuoteExcess { Description = "Standard Excess", Excess = 250.00m }, + new CarQuoteExcess { Description = "Voluntary Excess", Excess = 100.00m }, + new CarQuoteExcess { Description = "Young Driver Excess", Excess = 300.00m } + }; + + var carQuoteResponse = new CarQuoteResponse + { + Insurer = "TestInsurer", + Successful = true, + ElapsedMs = 1500, + TotalAnnual = 450.75m, + QuoteRef = "Q12345", + QuoteRefUrl = "https://example.com/quote/Q12345", + CanEmail = true, + EmailSent = false, + AddressText = "123 Test Street, Test City", + VehicleDescription = "2022 Test Car Model", + Excesses = excesses, + FailureReason = string.Empty, + CanDisplayFailure = false + }; + + var quoteResponse = new QuoteResponse + { + Id = id, + Insurer = "TestInsurer", + LiveTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 86400, // 24 hours from now + CarQuoteResponse = carQuoteResponse + }; + + // Override the table name + var config = new DynamoDBOperationConfig { OverrideTableName = TableName }; + await context.SaveAsync(quoteResponse, config); + + Console.WriteLine("Test data inserted successfully"); + } + + static async Task QueryData(DynamoDBContext context, string id) + { + Console.WriteLine($"Querying data with Id '{id}'..."); + + var config = new DynamoDBOperationConfig + { + OverrideTableName = TableName, + ConsistentRead = true + }; + + try + { + var results = await context.QueryAsync(id, config).GetRemainingAsync(); + + Console.WriteLine($"Successfully retrieved {results.Count} items"); + + if (results.Count > 0) + { + Console.WriteLine("Data successfully deserialized!"); + Console.WriteLine("Accessing the nested List..."); + + // Access the nested list - this should work with DynamicDependency attributes + var excesses = results[0].CarQuoteResponse.Excesses; + Console.WriteLine($"Excesses list contains {excesses.Count} items"); + + // Print each excess to prove it's working + foreach (var excess in excesses) + { + Console.WriteLine($" * {excess.Description}: {excess.Excess:C}"); + } + } + else + { + Console.WriteLine("No items found with the given ID."); + } + } + catch (Exception) + { + Console.WriteLine("Failed to deserialize the List property"); + throw; // Re-throw to be caught by the outer try/catch + } + } +} diff --git a/TestAotDeserialization_WithFix/TestAotDeserialization_WithFix.csproj b/TestAotDeserialization_WithFix/TestAotDeserialization_WithFix.csproj new file mode 100644 index 000000000000..fc9faf7f25ad --- /dev/null +++ b/TestAotDeserialization_WithFix/TestAotDeserialization_WithFix.csproj @@ -0,0 +1,17 @@ + + + + Exe + net8.0 + enable + enable + true + true + + + + + + + +