diff --git a/.autover/changes/d2fd7dc5-4c90-4697-8dbe-297f6e15cf88.json b/.autover/changes/d2fd7dc5-4c90-4697-8dbe-297f6e15cf88.json new file mode 100644 index 00000000..ef97328b --- /dev/null +++ b/.autover/changes/d2fd7dc5-4c90-4697-8dbe-297f6e15cf88.json @@ -0,0 +1,29 @@ +{ + "Projects": [ + { + "Name": "AWS.Messaging", + "Type": "Minor", + "ChangelogMessages": [ + "Update .NET target to .NET 8", + "Add trimming support for Native AOT compatiblity" + ] + }, + { + "Name": "AWS.Messaging.Lambda", + "Type": "Minor", + "ChangelogMessages": [ + "Update .NET target to .NET 8", + "Add trimming support for Native AOT compatiblity" + ] + }, + { + "Name": "AWS.Messaging.Telemetry.OpenTelemetry", + "Type": "Minor", + "ChangelogMessages": [ + "Update .NET target to .NET 8", + "Add trimming support for Native AOT compatiblity" + ] + } + + ] +} \ No newline at end of file diff --git a/sampleapps/LambdaMessaging/LambdaMessaging.csproj b/sampleapps/LambdaMessaging/LambdaMessaging.csproj index d1a946f0..c3920e3f 100644 --- a/sampleapps/LambdaMessaging/LambdaMessaging.csproj +++ b/sampleapps/LambdaMessaging/LambdaMessaging.csproj @@ -1,6 +1,6 @@ - net6.0 + net8.0 enable enable true diff --git a/sampleapps/LambdaMessaging/Properties/launchSettings.json b/sampleapps/LambdaMessaging/Properties/launchSettings.json index a82d4042..c1fabf8f 100644 --- a/sampleapps/LambdaMessaging/Properties/launchSettings.json +++ b/sampleapps/LambdaMessaging/Properties/launchSettings.json @@ -3,8 +3,8 @@ "Mock Lambda Test Tool": { "commandName": "Executable", "commandLineArgs": "--port 5050", - "workingDirectory": ".\\bin\\$(Configuration)\\net6.0", - "executablePath": "%USERPROFILE%\\.dotnet\\tools\\dotnet-lambda-test-tool-6.0.exe" + "workingDirectory": ".\\bin\\$(Configuration)\\net8.0", + "executablePath": "%USERPROFILE%\\.dotnet\\tools\\dotnet-lambda-test-tool-8.0.exe" } } } \ No newline at end of file diff --git a/sampleapps/PollyIntegration/PollyIntegration.csproj b/sampleapps/PollyIntegration/PollyIntegration.csproj index 11b14630..25a1e842 100644 --- a/sampleapps/PollyIntegration/PollyIntegration.csproj +++ b/sampleapps/PollyIntegration/PollyIntegration.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 enable enable diff --git a/sampleapps/PublisherAPI/PublisherAPI.csproj b/sampleapps/PublisherAPI/PublisherAPI.csproj index 9d9f42c5..bb1b0844 100644 --- a/sampleapps/PublisherAPI/PublisherAPI.csproj +++ b/sampleapps/PublisherAPI/PublisherAPI.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable diff --git a/sampleapps/SubscriberService/SubscriberService.csproj b/sampleapps/SubscriberService/SubscriberService.csproj index de382e3b..6af37aac 100644 --- a/sampleapps/SubscriberService/SubscriberService.csproj +++ b/sampleapps/SubscriberService/SubscriberService.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 enable enable diff --git a/src/AWS.Messaging.Lambda/AWS.Messaging.Lambda.csproj b/src/AWS.Messaging.Lambda/AWS.Messaging.Lambda.csproj index 4fd5c16f..aec5c7be 100644 --- a/src/AWS.Messaging.Lambda/AWS.Messaging.Lambda.csproj +++ b/src/AWS.Messaging.Lambda/AWS.Messaging.Lambda.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 true Amazon Web Services Amazon Web Services @@ -21,6 +21,7 @@ true true snupkg + true diff --git a/src/AWS.Messaging.Lambda/Services/DefaultLambdaMessageProcessor.cs b/src/AWS.Messaging.Lambda/Services/DefaultLambdaMessageProcessor.cs index 673a7b85..9a0d0477 100644 --- a/src/AWS.Messaging.Lambda/Services/DefaultLambdaMessageProcessor.cs +++ b/src/AWS.Messaging.Lambda/Services/DefaultLambdaMessageProcessor.cs @@ -247,18 +247,21 @@ public async Task DeleteMessagesAsync(IEnumerable messages, Can var response = await _sqsClient.DeleteMessageBatchAsync(request, token); - var successFulResponse = response.Successful ?? new List(); - var failedResponse = response.Failed ?? new List(); - - foreach (var successMessage in successFulResponse) + if (response.Successful != null) { - _logger.LogTrace("Deleted message {MessageId} from queue {SubscriberEndpoint} successfully", successMessage.Id, _configuration.SubscriberEndpoint); + foreach (var successMessage in response.Successful) + { + _logger.LogTrace("Deleted message {MessageId} from queue {SubscriberEndpoint} successfully", successMessage.Id, _configuration.SubscriberEndpoint); + } } - foreach (var failedMessage in failedResponse) + if (response.Failed != null) { - _logger.LogError("Failed to delete message {FailedMessageId} from queue {SubscriberEndpoint}: {FailedMessage}", - failedMessage.Id, _configuration.SubscriberEndpoint, failedMessage.Message); + foreach (var failedMessage in response.Failed) + { + _logger.LogError("Failed to delete message {FailedMessageId} from queue {SubscriberEndpoint}: {FailedMessage}", + failedMessage.Id, _configuration.SubscriberEndpoint, failedMessage.Message); + } } } diff --git a/src/AWS.Messaging.Telemetry.OpenTelemetry/AWS.Messaging.Telemetry.OpenTelemetry.csproj b/src/AWS.Messaging.Telemetry.OpenTelemetry/AWS.Messaging.Telemetry.OpenTelemetry.csproj index 286a843e..810a3739 100644 --- a/src/AWS.Messaging.Telemetry.OpenTelemetry/AWS.Messaging.Telemetry.OpenTelemetry.csproj +++ b/src/AWS.Messaging.Telemetry.OpenTelemetry/AWS.Messaging.Telemetry.OpenTelemetry.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable true @@ -23,6 +23,7 @@ true true snupkg + true diff --git a/src/AWS.Messaging.Telemetry.OpenTelemetry/OpenTelemetryTrace.cs b/src/AWS.Messaging.Telemetry.OpenTelemetry/OpenTelemetryTrace.cs index dd38f793..2cca6e9b 100644 --- a/src/AWS.Messaging.Telemetry.OpenTelemetry/OpenTelemetryTrace.cs +++ b/src/AWS.Messaging.Telemetry.OpenTelemetry/OpenTelemetryTrace.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Text.Json; +using AWS.Messaging.Internal; using OpenTelemetry; using OpenTelemetry.Context.Propagation; using OpenTelemetry.Trace; @@ -74,7 +75,7 @@ public void RecordTelemetryContext(MessageEnvelope envelope) /// Context value private void InjectTraceContextIntoEnvelope(MessageEnvelope envelope, string key, string value) { - envelope.Metadata[key] = JsonSerializer.SerializeToElement(value); + envelope.Metadata[key] = JsonSerializer.SerializeToElement(value, typeof(string), MessagingJsonSerializerContext.Default); } private bool _disposed; diff --git a/src/AWS.Messaging/AWS.Messaging.csproj b/src/AWS.Messaging/AWS.Messaging.csproj index 3f9327be..4c9f5990 100644 --- a/src/AWS.Messaging/AWS.Messaging.csproj +++ b/src/AWS.Messaging/AWS.Messaging.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 true AWS Message Processing Framework Amazon Web Services @@ -15,6 +15,7 @@ README.md True CA1727 + CS1591 true ..\..\public.snk 2.0.0-preview.1 @@ -22,13 +23,14 @@ true true snupkg + true - - - - + + + + diff --git a/src/AWS.Messaging/Configuration/IMessageBusBuilder.cs b/src/AWS.Messaging/Configuration/IMessageBusBuilder.cs index a4c2b89b..06a3815d 100644 --- a/src/AWS.Messaging/Configuration/IMessageBusBuilder.cs +++ b/src/AWS.Messaging/Configuration/IMessageBusBuilder.cs @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics.CodeAnalysis; using AWS.Messaging.Publishers.EventBridge; using AWS.Messaging.Publishers.SNS; using AWS.Messaging.Publishers.SQS; @@ -23,7 +24,7 @@ public interface IMessageBusBuilder /// The SQS queue URL to publish the message to. If the queue URL is null, a message-specific queue /// URL must be specified on the when sending a message. /// The language-agnostic message type identifier. If not specified, the .NET type will be used. - IMessageBusBuilder AddSQSPublisher(string? queueUrl, string? messageTypeIdentifier = null); + IMessageBusBuilder AddSQSPublisher<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TMessage>(string? queueUrl, string? messageTypeIdentifier = null); /// /// Adds an SNS Publisher to the framework which will handle publishing @@ -32,7 +33,7 @@ public interface IMessageBusBuilder /// The SNS topic URL to publish the message to. If the topic URL is null, a message-specific /// topic URL must be set on the when publishing a message. /// The language-agnostic message type identifier. If not specified, the .NET type will be used. - IMessageBusBuilder AddSNSPublisher(string? topicUrl, string? messageTypeIdentifier = null); + IMessageBusBuilder AddSNSPublisher<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TMessage>(string? topicUrl, string? messageTypeIdentifier = null); /// /// Adds an EventBridge Publisher to the framework which will handle publishing the defined message type to the specified EventBridge event bus name. @@ -42,14 +43,14 @@ public interface IMessageBusBuilder /// a message-specific event bus must be set on the when sending an event. /// The language-agnostic message type identifier. If not specified, the .NET type will be used. /// Contains additional properties that can be set while configuring an EventBridge publisher - IMessageBusBuilder AddEventBridgePublisher(string? eventBusName, string? messageTypeIdentifier = null, EventBridgePublishOptions? options = null); + IMessageBusBuilder AddEventBridgePublisher<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TMessage>(string? eventBusName, string? messageTypeIdentifier = null, EventBridgePublishOptions? options = null); /// /// Add a message handler for a given message type. /// The message handler contains the business logic of how to process a given message type. /// /// The language-agnostic message type identifier. If not specified, the .NET type will be used. - IMessageBusBuilder AddMessageHandler(string? messageTypeIdentifier = null) + IMessageBusBuilder AddMessageHandler<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THandler, TMessage>(string? messageTypeIdentifier = null) where THandler : IMessageHandler; /// @@ -89,6 +90,8 @@ IMessageBusBuilder AddMessageHandler(string? messageTypeIden /// and apply the Message bus configuration based on that section. /// /// + [RequiresDynamicCode("This method requires loading types dynamically as defined in the configuration system.")] + [RequiresUnreferencedCode("This method requires loading types dynamically as defined in the configuration system.")] IMessageBusBuilder LoadConfigurationFromSettings(IConfiguration configuration); /// diff --git a/src/AWS.Messaging/Configuration/MessageBusBuilder.cs b/src/AWS.Messaging/Configuration/MessageBusBuilder.cs index 22a4dd23..869419ed 100644 --- a/src/AWS.Messaging/Configuration/MessageBusBuilder.cs +++ b/src/AWS.Messaging/Configuration/MessageBusBuilder.cs @@ -1,22 +1,23 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using AWS.Messaging.Configuration.Internal; +using AWS.Messaging.Publishers; +using AWS.Messaging.Publishers.EventBridge; +using AWS.Messaging.Publishers.SNS; +using AWS.Messaging.Publishers.SQS; using AWS.Messaging.Serialization; using AWS.Messaging.Services; -using AWS.Messaging.Publishers; +using AWS.Messaging.Services.Backoff; +using AWS.Messaging.Services.Backoff.Policies; +using AWS.Messaging.Telemetry; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using AWS.Messaging.Publishers.SQS; -using AWS.Messaging.Publishers.SNS; -using AWS.Messaging.Publishers.EventBridge; using Microsoft.Extensions.Configuration; -using AWS.Messaging.Configuration.Internal; -using System.Reflection; -using AWS.Messaging.Services.Backoff; -using AWS.Messaging.Services.Backoff.Policies; -using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging; -using AWS.Messaging.Telemetry; +using Microsoft.Extensions.Logging.Abstractions; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; namespace AWS.Messaging.Configuration; @@ -48,36 +49,36 @@ public MessageBusBuilder(IServiceCollection services) } /// - public IMessageBusBuilder AddSQSPublisher(string? queueUrl, string? messageTypeIdentifier = null) + public IMessageBusBuilder AddSQSPublisher<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TMessage>(string? queueUrl, string? messageTypeIdentifier = null) { return AddSQSPublisher(typeof(TMessage), queueUrl, messageTypeIdentifier); } - private IMessageBusBuilder AddSQSPublisher(Type messageType, string? queueUrl, string? messageTypeIdentifier = null) + private IMessageBusBuilder AddSQSPublisher([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type messageType, string? queueUrl, string? messageTypeIdentifier = null) { var sqsPublisherConfiguration = new SQSPublisherConfiguration(queueUrl); return AddPublisher(messageType, sqsPublisherConfiguration, PublisherTargetType.SQS_PUBLISHER, messageTypeIdentifier); } /// - public IMessageBusBuilder AddSNSPublisher(string? topicUrl, string? messageTypeIdentifier = null) + public IMessageBusBuilder AddSNSPublisher<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TMessage>(string? topicUrl, string? messageTypeIdentifier = null) { return AddSNSPublisher(typeof(TMessage), topicUrl, messageTypeIdentifier); } - private IMessageBusBuilder AddSNSPublisher(Type messageType, string? topicUrl, string? messageTypeIdentifier = null) + private IMessageBusBuilder AddSNSPublisher([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type messageType, string? topicUrl, string? messageTypeIdentifier = null) { var snsPublisherConfiguration = new SNSPublisherConfiguration(topicUrl); return AddPublisher(messageType, snsPublisherConfiguration, PublisherTargetType.SNS_PUBLISHER, messageTypeIdentifier); } /// - public IMessageBusBuilder AddEventBridgePublisher(string? eventBusName, string? messageTypeIdentifier = null, EventBridgePublishOptions? options = null) + public IMessageBusBuilder AddEventBridgePublisher<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TMessage>(string? eventBusName, string? messageTypeIdentifier = null, EventBridgePublishOptions? options = null) { return AddEventBridgePublisher(typeof(TMessage), eventBusName, messageTypeIdentifier, options); } - private IMessageBusBuilder AddEventBridgePublisher(Type messageType, string? eventBusName, string? messageTypeIdentifier = null, EventBridgePublishOptions? options = null) + private IMessageBusBuilder AddEventBridgePublisher([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type messageType, string? eventBusName, string? messageTypeIdentifier = null, EventBridgePublishOptions? options = null) { var eventBridgePublisherConfiguration = new EventBridgePublisherConfiguration(eventBusName) { @@ -86,12 +87,7 @@ private IMessageBusBuilder AddEventBridgePublisher(Type messageType, string? eve return AddPublisher(messageType, eventBridgePublisherConfiguration, PublisherTargetType.EVENTBRIDGE_PUBLISHER, messageTypeIdentifier); } - private IMessageBusBuilder AddPublisher(IMessagePublisherConfiguration publisherConfiguration, string publisherType, string? messageTypeIdentifier = null) - { - return AddPublisher(typeof(TMessage), publisherConfiguration, publisherType, messageTypeIdentifier); - } - - private IMessageBusBuilder AddPublisher(Type messageType, IMessagePublisherConfiguration publisherConfiguration, string publisherType, string? messageTypeIdentifier = null) + private IMessageBusBuilder AddPublisher([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type messageType, IMessagePublisherConfiguration publisherConfiguration, string publisherType, string? messageTypeIdentifier = null) { var publisherMapping = new PublisherMapping(messageType, publisherConfiguration, publisherType, messageTypeIdentifier); _messageConfiguration.PublisherMappings.Add(publisherMapping); @@ -99,19 +95,15 @@ private IMessageBusBuilder AddPublisher(Type messageType, IMessagePublisherConfi } /// - public IMessageBusBuilder AddMessageHandler(string? messageTypeIdentifier = null) + public IMessageBusBuilder AddMessageHandler<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THandler, TMessage>(string? messageTypeIdentifier = null) where THandler : IMessageHandler { - return AddMessageHandler(typeof(THandler), typeof(TMessage), messageTypeIdentifier); + return AddMessageHandler(typeof(THandler), typeof(TMessage), () => new MessageEnvelope(), messageTypeIdentifier); } - private IMessageBusBuilder AddMessageHandler(Type handlerType, Type messageType, string? messageTypeIdentifier = null) + private IMessageBusBuilder AddMessageHandler([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type handlerType, Type messageType, Func envelopeFactory, string? messageTypeIdentifier = null) { - Type genericMessageHandler = typeof(IMessageHandler<>).MakeGenericType(messageType); - if (!handlerType.GetInterfaces().Any(x => x.Equals(genericMessageHandler))) - throw new InvalidMessageHandlerTypeException("The handler type should implement 'IMessageHandler'."); - - var subscriberMapping = new SubscriberMapping(handlerType, messageType, messageTypeIdentifier); + var subscriberMapping = new SubscriberMapping(handlerType, messageType, envelopeFactory, messageTypeIdentifier); _messageConfiguration.SubscriberMappings.Add(subscriberMapping); return this; } @@ -173,6 +165,8 @@ public IMessageBusBuilder AddMessageSourceSuffix(string suffix) } /// + [RequiresDynamicCode("This method requires loading types dynamically as defined in the configuration system.")] + [RequiresUnreferencedCode("This method requires loading types dynamically as defined in the configuration system.")] public IMessageBusBuilder LoadConfigurationFromSettings(IConfiguration configuration) { // This call needs to happen in this function so that the calling assembly is the customer's assembly. @@ -225,7 +219,23 @@ public IMessageBusBuilder LoadConfigurationFromSettings(IConfiguration configura var handlerType = GetTypeFromAssemblies(callingAssembly, messageHandler.HandlerType); if (handlerType is null) throw new InvalidAppSettingsConfigurationException($"Unable to find the provided message handler type '{messageHandler.HandlerType}'."); - AddMessageHandler(handlerType, messageType, messageHandler.MessageTypeIdentifier); + + // This func is not Native AOT compatible but the method in general is marked + // as not being Native AOT compatible due to loading dynamic types. So this + // func not being Native AOT compatible is okay. + var envelopeFactory = () => + { + var messageEnvelopeType = typeof(MessageEnvelope<>).MakeGenericType(messageType); + var envelope = Activator.CreateInstance(messageEnvelopeType); + if (envelope == null || envelope is not MessageEnvelope) + { + throw new InvalidOperationException($"Failed to create a {nameof(MessageEnvelope)} of type '{messageEnvelopeType.FullName}'"); + + } + return (MessageEnvelope)envelope; + }; + + AddMessageHandler(handlerType, messageType, envelopeFactory, messageHandler.MessageTypeIdentifier); } } @@ -272,6 +282,7 @@ public IMessageBusBuilder LoadConfigurationFromSettings(IConfiguration configura return this; } + [RequiresUnreferencedCode("This method requires loading types dynamically as defined in the configuration system.")] private Type? GetTypeFromAssemblies(Assembly callingAssembly, string typeValue) { if (typeValue.Contains(',')) diff --git a/src/AWS.Messaging/Configuration/SerializerOptions.cs b/src/AWS.Messaging/Configuration/SerializerOptions.cs index 07d90242..c207d02a 100644 --- a/src/AWS.Messaging/Configuration/SerializerOptions.cs +++ b/src/AWS.Messaging/Configuration/SerializerOptions.cs @@ -1,13 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Text.Json; using System.Text.Json.Serialization; -using System.Threading.Tasks; namespace AWS.Messaging.Configuration; @@ -20,8 +15,20 @@ public class SerializationOptions /// /// This is an instance of that controls the serialization/de-serialization logic of the application message. /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", + Justification = "Consumers relying on trimming would have been required to call the AddAWSMessageBus overload that takes in JsonSerializerContext that will be used here to avoid the call that requires unreferenced code.")] public JsonSerializerOptions? SystemTextJsonOptions { get; set; } = new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } }; + + /// + /// Default constructor + /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", + Justification = "Consumers relying on trimming would have been required to call the AddAWSMessageBus overload that takes in JsonSerializerContext that will be used here to avoid the call that requires unreferenced code.")] + public SerializationOptions() + { + + } } diff --git a/src/AWS.Messaging/Configuration/SubscriberMapping.cs b/src/AWS.Messaging/Configuration/SubscriberMapping.cs index 8ec06d35..532005e1 100644 --- a/src/AWS.Messaging/Configuration/SubscriberMapping.cs +++ b/src/AWS.Messaging/Configuration/SubscriberMapping.cs @@ -1,6 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics.CodeAnalysis; + namespace AWS.Messaging.Configuration; /// @@ -9,6 +11,7 @@ namespace AWS.Messaging.Configuration; public class SubscriberMapping { /// + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] public Type HandlerType { get; } /// @@ -17,13 +20,21 @@ public class SubscriberMapping /// public string MessageTypeIdentifier { get; } + // The MessageEnvelopeFactory func is used to create func from the generic parameters + // of the public interface where the types are known at compile time. This allows + // Native AOT compatibility and avoid having to make reflection calls like Type.MakeGeneric + // which is not compatible with Native AOT. + internal Func MessageEnvelopeFactory { get; } + /// /// Constructs an instance of /// /// The type that implements /// The type that will be message data will deserialized into + /// Func for creating MessageEnvelope<messageType> /// Optional message type identifier. If not set the full name of the is used. - public SubscriberMapping(Type handlerType, Type messageType, string? messageTypeIdentifier = null) + + internal SubscriberMapping([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type handlerType, Type messageType, Func envelopeFactory, string? messageTypeIdentifier = null) { HandlerType = handlerType; MessageType = messageType; @@ -31,5 +42,25 @@ public SubscriberMapping(Type handlerType, Type messageType, string? messageType !string.IsNullOrEmpty(messageTypeIdentifier) ? messageTypeIdentifier : messageType.FullName ?? throw new InvalidMessageTypeException("Unable to retrieve the Full Name of the provided Message Type."); + + MessageEnvelopeFactory = envelopeFactory; + } + + /// + /// Creates a SubscriberMapping from the generic parameters for the handler and message. + /// + /// The type that implements + /// Func for creating MessageEnvelope<messageType> + /// + /// + public static SubscriberMapping Create<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THandler, TMessage>(string? messageTypeIdentifier = null) + where THandler : IMessageHandler + { + var envelopeFactory = () => + { + return new MessageEnvelope(); + }; + + return new SubscriberMapping(typeof(THandler), typeof(TMessage), envelopeFactory, messageTypeIdentifier); } } diff --git a/src/AWS.Messaging/Extensions/ServiceCollectionExtensions.cs b/src/AWS.Messaging/Extensions/ServiceCollectionExtensions.cs index 0206825f..c6226a19 100644 --- a/src/AWS.Messaging/Extensions/ServiceCollectionExtensions.cs +++ b/src/AWS.Messaging/Extensions/ServiceCollectionExtensions.cs @@ -2,6 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 using AWS.Messaging.Configuration; +using AWS.Messaging.Services; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; namespace Microsoft.Extensions.DependencyInjection; @@ -17,8 +20,42 @@ public static class ServiceCollectionExtensions /// /// /// An action to define the message framework configuration using + [RequiresUnreferencedCode("This API requires using unreferenced code for reflection based JSON serialization. Use AddAWSMessageBus overload that takes JsonSerializerContext parameter to avoid using unreferenced code.")] public static IServiceCollection AddAWSMessageBus(this IServiceCollection services, Action builder) { + return ConfigureMessagingServices(services, new NullMessageJsonSerializerContextContainer(), builder); + } + + /// + /// Adds the to the dependency injection framework + /// to allow access to the framework components throughout the application. + /// Allows the configuration of the messaging framework by exposing methods to add publishers and subscribers. + /// + /// When this overload is called with a the serialization and deserialization of .NET types to JSON messages will use + /// .NET's source generator implementation. Passing in a is required when using this library in Native AOT or other trimming environments. + /// + /// + /// The must have attributes for all .NET types used for serialization and deserialization into + /// messages. If the .NET types have any enum properties and the string representation of the enum should be used then add the + /// "[JsonSourceGenerationOptions(UseStringEnumConverter = true)]" attribute to the . + /// + /// + /// For more information about JSON source generator and the visit this link: + /// https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/source-generation + /// + /// + /// + /// The that will be used for serializing and deserializing the .NET types defined by consumers for representing messages. + /// An action to define the message framework configuration using + public static IServiceCollection AddAWSMessageBus(this IServiceCollection services, JsonSerializerContext jsonSerializerContext, Action builder) + { + return ConfigureMessagingServices(services, new DefaultMessageJsonSerializerContextContainer(jsonSerializerContext), builder); + } + + private static IServiceCollection ConfigureMessagingServices(IServiceCollection services, IMessageJsonSerializerContextContainer messageJsonSerializerContextFactory, Action builder) + { + services.AddSingleton(messageJsonSerializerContextFactory); + var messageBusBuilder = new MessageBusBuilder(services); builder(messageBusBuilder); diff --git a/src/AWS.Messaging/Internal/MessagingJsonSerializerContext.cs b/src/AWS.Messaging/Internal/MessagingJsonSerializerContext.cs new file mode 100644 index 00000000..e1a21e76 --- /dev/null +++ b/src/AWS.Messaging/Internal/MessagingJsonSerializerContext.cs @@ -0,0 +1,27 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Text.Json; +using System.Text.Json.Serialization; +using AWS.Messaging.Configuration.Internal; + +namespace AWS.Messaging.Internal; + +/// +/// The JsonSerializerContext used for any JSON serialization of types known by this library. The type is public +/// due to constraints of the source generator for JsonSerializerContext. +/// +[JsonSerializable(typeof(JsonElement))] +[JsonSerializable(typeof(string))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(Amazon.SQS.Model.MessageAttributeValue), TypeInfoPropertyName = "SQSMessageAttributeValue")] +[JsonSerializable(typeof(Dictionary), TypeInfoPropertyName = "DictionarySQSMessageAttributeValue")] +[JsonSerializable(typeof(Amazon.SimpleNotificationService.Model.MessageAttributeValue), TypeInfoPropertyName = "SNSMessageAttributeValue")] +[JsonSerializable(typeof(Dictionary), TypeInfoPropertyName = "DictionarySNSMessageAttributeValue")] +[JsonSerializable(typeof(MessageEnvelope))] +[JsonSerializable(typeof(DateTimeOffset))] +[JsonSerializable(typeof(TaskMetadataResponse))] +[JsonSourceGenerationOptions(UseStringEnumConverter = true)] +public partial class MessagingJsonSerializerContext : JsonSerializerContext +{ +} diff --git a/src/AWS.Messaging/SQS/SQSMessagePoller.cs b/src/AWS.Messaging/SQS/SQSMessagePoller.cs index bf9dfdda..11ee4487 100644 --- a/src/AWS.Messaging/SQS/SQSMessagePoller.cs +++ b/src/AWS.Messaging/SQS/SQSMessagePoller.cs @@ -249,18 +249,21 @@ public async Task DeleteMessagesAsync(IEnumerable messages, Can { var response = await _sqsClient.DeleteMessageBatchAsync(request, token); - var successfulResponses = response.Successful ?? new List(); - var failedResponses = response.Failed ?? new List(); - - foreach (var successMessage in successfulResponses) + if (response.Successful != null) { - _logger.LogTrace("Deleted message {MessageId} from queue {SubscriberEndpoint} successfully", successMessage.Id, _configuration.SubscriberEndpoint); + foreach (var successMessage in response.Successful) + { + _logger.LogTrace("Deleted message {MessageId} from queue {SubscriberEndpoint} successfully", successMessage.Id, _configuration.SubscriberEndpoint); + } } - foreach (var failedMessage in failedResponses) + if (response.Failed != null) { - _logger.LogError("Failed to delete message {FailedMessageId} from queue {SubscriberEndpoint}: {FailedMessage}", - failedMessage.Id, _configuration.SubscriberEndpoint, failedMessage.Message); + foreach (var failedMessage in response.Failed) + { + _logger.LogError("Failed to delete message {FailedMessageId} from queue {SubscriberEndpoint}: {FailedMessage}", + failedMessage.Id, _configuration.SubscriberEndpoint, failedMessage.Message); + } } } catch (AmazonSQSException ex) @@ -352,29 +355,32 @@ public async Task ExtendMessageVisibilityTimeoutAsync(IEnumerable(); - var failedResponses = response.Failed ?? new List(); - - foreach (var successMessage in successFulResonses) + if (response.Successful != null) { - _logger.LogTrace("Extended the visibility of message {MessageId} on queue {SubscriberEndpoint} successfully", successMessage.Id, _configuration.SubscriberEndpoint); + foreach (var successMessage in response.Successful) + { + _logger.LogTrace("Extended the visibility of message {MessageId} on queue {SubscriberEndpoint} successfully", successMessage.Id, _configuration.SubscriberEndpoint); + } } - foreach (var failedMessage in failedResponses) + if (response.Failed != null) { - // It's possible that the task that is extending the message visibility timeout of in flight messages attempts to extend - // a message whose handler task has just finished and deleted the message. Rather than adding synchronization between the two - // (such as stopping handlers from deleting while the extension task is running), we "downgrade" these errors to trace messages - if (failedMessage.Code.Equals("ReceiptHandleIsInvalid") && - failedMessage.Message.Equals("Message does not exist or is not available for visibility timeout change", StringComparison.InvariantCultureIgnoreCase)) - { - _logger.LogTrace("Failed to extend the visibility of message {FailedMessageId} on queue {SubscriberEndpoint} with code {Code}, which was likely deleted: {FailedMessage}", - failedMessage.Id, _configuration.SubscriberEndpoint, failedMessage.Code, failedMessage.Message); - } - else // treat any other failed entries as errors + foreach (var failedMessage in response.Failed) { - _logger.LogError("Failed to extend the visibility of message {FailedMessageId} on queue {SubscriberEndpoint} with code {Code}: {FailedMessage}", - failedMessage.Id, _configuration.SubscriberEndpoint, failedMessage.Code, failedMessage.Message); + // It's possible that the task that is extending the message visibility timeout of in flight messages attempts to extend + // a message whose handler task has just finished and deleted the message. Rather than adding synchronization between the two + // (such as stopping handlers from deleting while the extension task is running), we "downgrade" these errors to trace messages + if (failedMessage.Code.Equals("ReceiptHandleIsInvalid") && + failedMessage.Message.Equals("Message does not exist or is not available for visibility timeout change", StringComparison.InvariantCultureIgnoreCase)) + { + _logger.LogTrace("Failed to extend the visibility of message {FailedMessageId} on queue {SubscriberEndpoint} with code {Code}, which was likely deleted: {FailedMessage}", + failedMessage.Id, _configuration.SubscriberEndpoint, failedMessage.Code, failedMessage.Message); + } + else // treat any other failed entries as errors + { + _logger.LogError("Failed to extend the visibility of message {FailedMessageId} on queue {SubscriberEndpoint} with code {Code}: {FailedMessage}", + failedMessage.Id, _configuration.SubscriberEndpoint, failedMessage.Code, failedMessage.Message); + } } } } diff --git a/src/AWS.Messaging/Serialization/EnvelopeSerializer.cs b/src/AWS.Messaging/Serialization/EnvelopeSerializer.cs index d95ec198..0ebb9d70 100644 --- a/src/AWS.Messaging/Serialization/EnvelopeSerializer.cs +++ b/src/AWS.Messaging/Serialization/EnvelopeSerializer.cs @@ -4,8 +4,10 @@ using System.Text; using System.Text.Json; using System.Text.Json.Nodes; +using System.Text.Json.Serialization.Metadata; using Amazon.SQS.Model; using AWS.Messaging.Configuration; +using AWS.Messaging.Internal; using AWS.Messaging.Services; using Microsoft.Extensions.Logging; @@ -102,7 +104,7 @@ public async ValueTask SerializeAsync(MessageEnvelope envelope) { if (!blob.ContainsKey(key)) // don't overwrite any reserved keys { - blob[key] = JsonSerializer.SerializeToNode(envelope.Metadata[key]); + blob[key] = JsonSerializer.SerializeToNode(envelope.Metadata[key], typeof(JsonElement), MessagingJsonSerializerContext.Default); } } @@ -138,7 +140,7 @@ public async ValueTask ConvertToEnvelopeAsync(Message s { sqsMessage.Body = await InvokePreDeserializationCallback(sqsMessage.Body); var messageEnvelopeConfiguration = GetMessageEnvelopeConfiguration(sqsMessage); - var intermediateEnvelope = JsonSerializer.Deserialize>(messageEnvelopeConfiguration.MessageEnvelopeBody!)!; + var intermediateEnvelope = JsonSerializer.Deserialize>(messageEnvelopeConfiguration.MessageEnvelopeBody!, MessagingJsonSerializerContext.Default.MessageEnvelopeString)!; ValidateMessageEnvelope(intermediateEnvelope); var messageTypeIdentifier = intermediateEnvelope.MessageTypeIdentifier; var subscriberMapping = _messageConfiguration.GetSubscriberMapping(messageTypeIdentifier); @@ -150,13 +152,7 @@ public async ValueTask ConvertToEnvelopeAsync(Message s var messageType = subscriberMapping.MessageType; var message = _messageSerializer.Deserialize(intermediateEnvelope.Message, messageType); - var messageEnvelopeType = typeof(MessageEnvelope<>).MakeGenericType(messageType); - - if (Activator.CreateInstance(messageEnvelopeType) is not MessageEnvelope finalMessageEnvelope) - { - _logger.LogError($"Failed to create a {nameof(MessageEnvelope)} of type '{{MessageEnvelopeType}}'", messageEnvelopeType.FullName); - throw new InvalidOperationException($"Failed to create a {nameof(MessageEnvelope)} of type '{messageEnvelopeType.FullName}'"); - } + var finalMessageEnvelope = subscriberMapping.MessageEnvelopeFactory.Invoke(); finalMessageEnvelope.Id = intermediateEnvelope.Id; finalMessageEnvelope.Source = intermediateEnvelope.Source; @@ -272,15 +268,11 @@ private void SetSQSMetadata(MessageEnvelopeConfiguration envelopeConfiguration, MessageAttributes = sqsMessage.MessageAttributes, ReceiptHandle = sqsMessage.ReceiptHandle }; - if (sqsMessage.Attributes is null || sqsMessage.Attributes.Count == 0) - { - return; - } - if (sqsMessage.Attributes.TryGetValue("MessageGroupId", out var attribute)) + if (sqsMessage.Attributes != null && sqsMessage.Attributes.TryGetValue("MessageGroupId", out var attribute)) { envelopeConfiguration.SQSMetadata.MessageGroupId = attribute; } - if (sqsMessage.Attributes.TryGetValue("MessageDeduplicationId", out var messageAttribute)) + if (sqsMessage.Attributes != null && sqsMessage.Attributes.TryGetValue("MessageDeduplicationId", out var messageAttribute)) { envelopeConfiguration.SQSMetadata.MessageDeduplicationId = messageAttribute; } @@ -298,7 +290,7 @@ private void SetSNSMetadata(MessageEnvelopeConfiguration envelopeConfiguration, }; if (root.TryGetProperty("MessageAttributes", out var messageAttributes)) { - envelopeConfiguration.SNSMetadata.MessageAttributes = messageAttributes.Deserialize>(); + envelopeConfiguration.SNSMetadata.MessageAttributes = messageAttributes.Deserialize(MessagingJsonSerializerContext.Default.DictionarySNSMessageAttributeValue); } } @@ -337,7 +329,7 @@ private DateTimeOffset GetJsonPropertyAsDateTimeOffset(JsonElement node, string { if (node.TryGetProperty(propertyName, out var propertyValue)) { - return JsonSerializer.Deserialize(propertyValue); + return JsonSerializer.Deserialize(propertyValue, MessagingJsonSerializerContext.Default.DateTimeOffset); } return DateTimeOffset.MinValue; } @@ -346,7 +338,12 @@ private DateTimeOffset GetJsonPropertyAsDateTimeOffset(JsonElement node, string { if (node.TryGetProperty(propertyName, out var propertyValue)) { - return JsonSerializer.Deserialize>(propertyValue); + var jsonTypeInfo = MessagingJsonSerializerContext.Default.GetTypeInfo(typeof(List)) as JsonTypeInfo>; + if (jsonTypeInfo == null) + { + throw new InvalidOperationException($"Missing JsonSerializable registeration for type {typeof(List).FullName}"); + } + return JsonSerializer.Deserialize>(propertyValue, jsonTypeInfo); } return null; } diff --git a/src/AWS.Messaging/Serialization/MessageSerializer.cs b/src/AWS.Messaging/Serialization/MessageSerializer.cs index 9324c6df..6084e1d9 100644 --- a/src/AWS.Messaging/Serialization/MessageSerializer.cs +++ b/src/AWS.Messaging/Serialization/MessageSerializer.cs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 using System.Text.Json; +using System.Text.Json.Serialization; using AWS.Messaging.Configuration; +using AWS.Messaging.Services; using Microsoft.Extensions.Logging; namespace AWS.Messaging.Serialization; @@ -15,15 +17,21 @@ internal class MessageSerializer : IMessageSerializer { private readonly ILogger _logger; private readonly IMessageConfiguration _messageConfiguration; + private readonly JsonSerializerContext? _jsonSerializerContext; - public MessageSerializer(ILogger logger, IMessageConfiguration messageConfiguration) + public MessageSerializer(ILogger logger, IMessageConfiguration messageConfiguration, IMessageJsonSerializerContextContainer jsonContextContainer) { _logger = logger; _messageConfiguration= messageConfiguration; + _jsonSerializerContext = jsonContextContainer.GetJsonSerializerContext(); } /// /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", + Justification = "Consumers relying on trimming would have been required to call the AddAWSMessageBus overload that takes in JsonSerializerContext that will be used here to avoid the call that requires unreferenced code.")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", + Justification = "Consumers relying on trimming would have been required to call the AddAWSMessageBus overload that takes in JsonSerializerContext that will be used here to avoid the call that requires unreferenced code.")] public object Deserialize(string message, Type deserializedType) { try @@ -38,7 +46,14 @@ public object Deserialize(string message, Type deserializedType) _logger.LogTrace("Deserializing the following message into type '{DeserializedType}'", deserializedType); } - return JsonSerializer.Deserialize(message, deserializedType, jsonSerializerOptions) ?? throw new JsonException("The deserialized application message is null."); + if (_jsonSerializerContext != null) + { + return JsonSerializer.Deserialize(message, deserializedType, _jsonSerializerContext) ?? throw new JsonException("The deserialized application message is null."); + } + else + { + return JsonSerializer.Deserialize(message, deserializedType, jsonSerializerOptions) ?? throw new JsonException("The deserialized application message is null."); + } } catch (JsonException) when (!_messageConfiguration.LogMessageContent) { @@ -54,12 +69,28 @@ public object Deserialize(string message, Type deserializedType) /// /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", + Justification = "Consumers relying on trimming would have been required to call the AddAWSMessageBus overload that takes in JsonSerializerContext that will be used here to avoid the call that requires unreferenced code.")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", + Justification = "Consumers relying on trimming would have been required to call the AddAWSMessageBus overload that takes in JsonSerializerContext that will be used here to avoid the call that requires unreferenced code.")] public string Serialize(object message) { try { var jsonSerializerOptions = _messageConfiguration.SerializationOptions.SystemTextJsonOptions; - var jsonString = JsonSerializer.Serialize(message, jsonSerializerOptions); + + string jsonString; + Type messageType = message.GetType(); + + if (_jsonSerializerContext != null) + { + jsonString = JsonSerializer.Serialize(message, messageType, _jsonSerializerContext); + } + else + { + jsonString = JsonSerializer.Serialize(message, jsonSerializerOptions); + } + if (_messageConfiguration.LogMessageContent) { _logger.LogTrace("Serialized the message object as the following raw string:\n{JsonString}", jsonString); diff --git a/src/AWS.Messaging/Services/ECSContainerMetadataManager.cs b/src/AWS.Messaging/Services/ECSContainerMetadataManager.cs index c2176091..61bd9a07 100644 --- a/src/AWS.Messaging/Services/ECSContainerMetadataManager.cs +++ b/src/AWS.Messaging/Services/ECSContainerMetadataManager.cs @@ -2,9 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 using System.Text.Json; +using System.Text.Json.Serialization; using System.Text.RegularExpressions; using Amazon; using AWS.Messaging.Configuration.Internal; +using AWS.Messaging.Internal; using Microsoft.Extensions.Logging; namespace AWS.Messaging.Services; @@ -53,7 +55,7 @@ public ECSContainerMetadataManager( } var taskMetadataJson = await response.Content.ReadAsStringAsync(); - var taskMetadata = JsonSerializer.Deserialize(taskMetadataJson); + var taskMetadata = JsonSerializer.Deserialize(taskMetadataJson, MessagingJsonSerializerContext.Default.TaskMetadataResponse); if (ValidateContainerTaskMetadata(taskMetadata)) { return taskMetadata; @@ -106,4 +108,6 @@ private bool ValidateContainerTaskMetadata(TaskMetadataResponse? metadata) return true; } -} + + +} \ No newline at end of file diff --git a/src/AWS.Messaging/Services/IMessageJsonSerializerContextContainer.cs b/src/AWS.Messaging/Services/IMessageJsonSerializerContextContainer.cs new file mode 100644 index 00000000..5c37a70a --- /dev/null +++ b/src/AWS.Messaging/Services/IMessageJsonSerializerContextContainer.cs @@ -0,0 +1,49 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Text.Json.Serialization; + +namespace AWS.Messaging.Services; + +/// +/// Container for the JsonSerializerContext provided by users of the library. The JsonSerializerContext is +/// used when ever serializing/deserializing any of the .NET types consumers use to represent the messages. +/// +public interface IMessageJsonSerializerContextContainer +{ + /// + /// Returns the consumer provided JsonSerializerContext. + /// + /// + JsonSerializerContext? GetJsonSerializerContext(); +} + +public class NullMessageJsonSerializerContextContainer : IMessageJsonSerializerContextContainer +{ + /// + public JsonSerializerContext? GetJsonSerializerContext() => null; +} + +/// +/// The default implementation of IMessageJsonSerializerContextContainer when a user provides the library +/// a JsonSerializerContext to use for serializing/deserializing their types. +/// +public class DefaultMessageJsonSerializerContextContainer : IMessageJsonSerializerContextContainer +{ + private readonly JsonSerializerContext _jsonSerializerContext; + + /// + /// Create instance holding on to the JsonSerializerContext + /// + /// The user provided JsonSerializerContext. + public DefaultMessageJsonSerializerContextContainer(JsonSerializerContext jsonSerializerContext) + { + _jsonSerializerContext = jsonSerializerContext; + } + + /// + public JsonSerializerContext? GetJsonSerializerContext() + { + return _jsonSerializerContext; + } +} diff --git a/src/AWS.Messaging/Services/IMessageManagerFactory.cs b/src/AWS.Messaging/Services/IMessageManagerFactory.cs index dfc67607..adfc68ab 100644 --- a/src/AWS.Messaging/Services/IMessageManagerFactory.cs +++ b/src/AWS.Messaging/Services/IMessageManagerFactory.cs @@ -4,6 +4,7 @@ using AWS.Messaging.SQS; using AWS.Messaging.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace AWS.Messaging.Services; @@ -40,6 +41,13 @@ public DefaultMessageManagerFactory(IServiceProvider serviceProvider) /// public IMessageManager CreateMessageManager(ISQSMessageCommunication sqsMessageCommunication, MessageManagerConfiguration configuration) { - return ActivatorUtilities.CreateInstance(_serviceProvider, sqsMessageCommunication, configuration); + var manager = new DefaultMessageManager( + sqsMessageCommunication, + _serviceProvider.GetRequiredService(), + _serviceProvider.GetRequiredService().CreateLogger(), + configuration + ); + + return manager; } } diff --git a/test/AWS.Messaging.IntegrationTests/AWS.Messaging.IntegrationTests.csproj b/test/AWS.Messaging.IntegrationTests/AWS.Messaging.IntegrationTests.csproj index fe1f1bb0..be56be45 100644 --- a/test/AWS.Messaging.IntegrationTests/AWS.Messaging.IntegrationTests.csproj +++ b/test/AWS.Messaging.IntegrationTests/AWS.Messaging.IntegrationTests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 false enable true @@ -9,16 +9,16 @@ - - - - - + + + + + - - + + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/AWS.Messaging.Tests.Common/AWS.Messaging.Tests.Common.csproj b/test/AWS.Messaging.Tests.Common/AWS.Messaging.Tests.Common.csproj index fd696290..ddf559f2 100644 --- a/test/AWS.Messaging.Tests.Common/AWS.Messaging.Tests.Common.csproj +++ b/test/AWS.Messaging.Tests.Common/AWS.Messaging.Tests.Common.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable true @@ -9,12 +9,12 @@ - - - - - - + + + + + + @@ -24,4 +24,4 @@ - \ No newline at end of file + diff --git a/test/AWS.Messaging.Tests.LambdaFunctions/AWS.Messaging.Tests.LambdaFunctions.csproj b/test/AWS.Messaging.Tests.LambdaFunctions/AWS.Messaging.Tests.LambdaFunctions.csproj index ea300448..c36baa67 100644 --- a/test/AWS.Messaging.Tests.LambdaFunctions/AWS.Messaging.Tests.LambdaFunctions.csproj +++ b/test/AWS.Messaging.Tests.LambdaFunctions/AWS.Messaging.Tests.LambdaFunctions.csproj @@ -1,6 +1,6 @@ - net6.0 + net8.0 enable enable true @@ -14,9 +14,9 @@ - - - + + + diff --git a/test/AWS.Messaging.Tests.LambdaFunctions/Properties/launchSettings.json b/test/AWS.Messaging.Tests.LambdaFunctions/Properties/launchSettings.json index a82d4042..c1fabf8f 100644 --- a/test/AWS.Messaging.Tests.LambdaFunctions/Properties/launchSettings.json +++ b/test/AWS.Messaging.Tests.LambdaFunctions/Properties/launchSettings.json @@ -3,8 +3,8 @@ "Mock Lambda Test Tool": { "commandName": "Executable", "commandLineArgs": "--port 5050", - "workingDirectory": ".\\bin\\$(Configuration)\\net6.0", - "executablePath": "%USERPROFILE%\\.dotnet\\tools\\dotnet-lambda-test-tool-6.0.exe" + "workingDirectory": ".\\bin\\$(Configuration)\\net8.0", + "executablePath": "%USERPROFILE%\\.dotnet\\tools\\dotnet-lambda-test-tool-8.0.exe" } } } \ No newline at end of file diff --git a/test/AWS.Messaging.UnitTests/AWS.Messaging.UnitTests.csproj b/test/AWS.Messaging.UnitTests/AWS.Messaging.UnitTests.csproj index ff492c65..877b3383 100644 --- a/test/AWS.Messaging.UnitTests/AWS.Messaging.UnitTests.csproj +++ b/test/AWS.Messaging.UnitTests/AWS.Messaging.UnitTests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 false enable true diff --git a/test/AWS.Messaging.UnitTests/ConfigurationTests.cs b/test/AWS.Messaging.UnitTests/ConfigurationTests.cs index 3dad0424..a5bc12b2 100644 --- a/test/AWS.Messaging.UnitTests/ConfigurationTests.cs +++ b/test/AWS.Messaging.UnitTests/ConfigurationTests.cs @@ -13,14 +13,14 @@ public class ConfigurationTests [Fact] public void SubscriberMappingNoMessageIdentifier() { - var mapping = new SubscriberMapping(typeof(PurchaseOrderHandler), typeof(PurchaseOrder)); + var mapping = SubscriberMapping.Create(); Assert.Equal("AWS.Messaging.UnitTests.ConfigurationTests+PurchaseOrder", mapping.MessageTypeIdentifier); } [Fact] public void SubscriberMappingWithMessageIdentifier() { - var mapping = new SubscriberMapping(typeof(PurchaseOrderHandler), typeof(PurchaseOrder), "PO"); + var mapping = SubscriberMapping.Create("PO"); Assert.Equal("PO", mapping.MessageTypeIdentifier); } diff --git a/test/AWS.Messaging.UnitTests/DefaultMessageManagerTests.cs b/test/AWS.Messaging.UnitTests/DefaultMessageManagerTests.cs index c685ef97..d65cdbc1 100644 --- a/test/AWS.Messaging.UnitTests/DefaultMessageManagerTests.cs +++ b/test/AWS.Messaging.UnitTests/DefaultMessageManagerTests.cs @@ -35,7 +35,7 @@ public async Task DefaultMessageManager_ManagesMessageSuccess() var manager = new DefaultMessageManager(mockSQSMessageCommunication.Object, mockHandlerInvoker.Object, new NullLogger(), new MessageManagerConfiguration()); var messsageEnvelope = new MessageEnvelope { Id = "1" }; - var subscriberMapping = new SubscriberMapping(typeof(ChatMessageHandler), typeof(ChatMessage)); + var subscriberMapping = SubscriberMapping.Create(); await manager.ProcessMessageAsync(messsageEnvelope, subscriberMapping); @@ -68,7 +68,7 @@ public async Task DefaultMessageManager_ManagesMessageFailed() var manager = new DefaultMessageManager(mockSQSMessageCommunication.Object, mockHandlerInvoker.Object, new NullLogger(), new MessageManagerConfiguration()); var messsageEnvelope = new MessageEnvelope { Id = "1" }; - var subscriberMapping = new SubscriberMapping(typeof(ChatMessageHandler), typeof(ChatMessage)); + var subscriberMapping = SubscriberMapping.Create(); await manager.ProcessMessageAsync(messsageEnvelope, subscriberMapping); @@ -108,7 +108,7 @@ public async Task DefaultMessageManager_ExtendsVisibilityTimeout_Batch() VisibilityTimeoutExtensionHeartbeatInterval = 1 }); - var subscriberMapping = new SubscriberMapping(typeof(ChatMessageHandler), typeof(ChatMessage)); + var subscriberMapping = SubscriberMapping.Create(); // Start handling two messages at roughly the same time var message1 = new MessageEnvelope() { Id = "1" }; @@ -161,7 +161,7 @@ public async Task DefaultMessageManager_ExtendsVisibilityTimeout_OnlyWhenNecessa VisibilityTimeoutExtensionHeartbeatInterval = 1 }); - var subscriberMapping = new SubscriberMapping(typeof(ChatMessageHandler), typeof(ChatMessage)); + var subscriberMapping = SubscriberMapping.Create(); // Start handling a single message var earlyMessage = new MessageEnvelope() { Id = "1" }; @@ -212,7 +212,7 @@ public async Task DefaultMessageManager_CountsActiveMessagesCorrectly() var mockHandlerInvoker = CreateMockHandlerInvoker(MessageProcessStatus.Success(), TimeSpan.FromSeconds(1)); var manager = new DefaultMessageManager(mockSQSMessageCommunication.Object, mockHandlerInvoker.Object, new NullLogger(), new MessageManagerConfiguration()); - var subscriberMapping = new SubscriberMapping(typeof(ChatMessageHandler), typeof(ChatMessage)); + var subscriberMapping = SubscriberMapping.Create(); var tasks = new List(); @@ -244,7 +244,7 @@ public async Task DefaultMessageManager_ProcessMessageGroup() VisibilityTimeoutExtensionHeartbeatInterval = 1 }); - var subscriberMapping = new SubscriberMapping(typeof(ChatMessageHandler), typeof(ChatMessage)); + var subscriberMapping = SubscriberMapping.Create(); // Create 2 message groups "A" and "B" var message1 = new MessageEnvelope() { Id = "1" }; @@ -320,7 +320,7 @@ public async Task DefaultMessageManager_MessageGroupIsSkipped() VisibilityTimeoutExtensionHeartbeatInterval = 5 }); - var subscriberMapping = new SubscriberMapping(typeof(TransactionInfoHandler), typeof(TransactionInfoHandler)); + var subscriberMapping = SubscriberMapping.Create(); var messageGroup = new List { @@ -359,7 +359,7 @@ public async Task DefaultMessageManager_RethrowsInvalidMessageHandlerSignatureEx new MessageManagerConfiguration()); var messageEnvelope = new MessageEnvelope { Id = "1" }; - var subscriberMapping = new SubscriberMapping(typeof(ChatMessageHandler), typeof(ChatMessage)); + var subscriberMapping = SubscriberMapping.Create(); await Assert.ThrowsAsync(() => manager.ProcessMessageAsync(messageEnvelope, subscriberMapping)); diff --git a/test/AWS.Messaging.UnitTests/HandlerInvokerTests.cs b/test/AWS.Messaging.UnitTests/HandlerInvokerTests.cs index e1560188..2121c614 100644 --- a/test/AWS.Messaging.UnitTests/HandlerInvokerTests.cs +++ b/test/AWS.Messaging.UnitTests/HandlerInvokerTests.cs @@ -42,7 +42,7 @@ public async Task HandlerInvoker_HappyPath() new DefaultTelemetryFactory(serviceProvider)); var envelope = new MessageEnvelope(); - var subscriberMapping = new SubscriberMapping(typeof(ChatMessageHandler), typeof(ChatMessage)); + var subscriberMapping = SubscriberMapping.Create(); var messageProcessStatus = await handlerInvoker.InvokeAsync(envelope, subscriberMapping); Assert.Equal(MessageProcessStatus.Success(), messageProcessStatus); @@ -71,14 +71,14 @@ public async Task HandlerInvoker_DualHandler_InvokesCorrectMethod() // Assert that ChatMessage is routed to the right handler method, which always succeeds var chatEnvelope = new MessageEnvelope(); - var chatSubscriberMapping = new SubscriberMapping(typeof(DualHandler), typeof(ChatMessage)); + var chatSubscriberMapping = SubscriberMapping.Create(); var chatMessageProcessStatus = await handlerInvoker.InvokeAsync(chatEnvelope, chatSubscriberMapping); Assert.True(chatMessageProcessStatus.IsSuccess); // Assert that AddressInfo is routed to the right handler method, which always fails var addressEnvelope = new MessageEnvelope(); - var addressSubscriberMapping = new SubscriberMapping(typeof(DualHandler), typeof(AddressInfo)); + var addressSubscriberMapping = SubscriberMapping.Create(); var addressMessageProcessStatus = await handlerInvoker.InvokeAsync(addressEnvelope, addressSubscriberMapping); Assert.True(addressMessageProcessStatus.IsFailed); @@ -110,7 +110,7 @@ public async Task HandlerInvoker_UnwrapsTargetInvocationException() { Id = "123" }; - var subscriberMapping = new SubscriberMapping(typeof(ChatExceptionHandler), typeof(ChatMessage)); + var subscriberMapping = SubscriberMapping.Create(); await handlerInvoker.InvokeAsync(envelope, subscriberMapping); @@ -141,7 +141,7 @@ public async Task HandlerInvoker_VerifyHandlersAreRetrievedAsScopedDependencies( // ACT and ASSERT - Invoke the GreetingHandler multiple times and verify that a new instance of IGreeter is created each time. var envelope = new MessageEnvelope(); - var subscriberMapping = new SubscriberMapping(typeof(GreetingHandler), typeof(string)); + var subscriberMapping = SubscriberMapping.Create(); await handlerInvoker.InvokeAsync(envelope, subscriberMapping); await handlerInvoker.InvokeAsync(envelope, subscriberMapping); @@ -177,7 +177,7 @@ public async Task HandlerInvoker_VerifyHandlersFatalErrorWhenDIFails() new DefaultTelemetryFactory(serviceProvider)); var envelope = new MessageEnvelope(); - var subscriberMapping = new SubscriberMapping(typeof(ChatMessageHandlerWithDependencies), typeof(ChatMessage)); + var subscriberMapping = SubscriberMapping.Create(); await Assert.ThrowsAsync(async () => { await handlerInvoker.InvokeAsync(envelope, subscriberMapping); diff --git a/test/AWS.Messaging.UnitTests/MessagePublisherTests.cs b/test/AWS.Messaging.UnitTests/MessagePublisherTests.cs index 7b1a8030..59c0bf48 100644 --- a/test/AWS.Messaging.UnitTests/MessagePublisherTests.cs +++ b/test/AWS.Messaging.UnitTests/MessagePublisherTests.cs @@ -977,7 +977,7 @@ await messagePublisher.PublishAsync(_chatMessage, x.PutEventsAsync( It.Is(request => request.Entries[0].EventBusName.Equals("event-bus-123") && string.IsNullOrEmpty(request.EndpointId) - && request.Entries[0].TraceHeader.Equals("trace-header1") && ((DateTime)request.Entries[0].Time!).Year == dateTimeOffset.Year), + && request.Entries[0].TraceHeader.Equals("trace-header1") && request.Entries[0].Time.Value.Year == dateTimeOffset.Year), It.IsAny()), Times.Exactly(1)); } diff --git a/test/AWS.Messaging.UnitTests/OpenTelemetryTests.cs b/test/AWS.Messaging.UnitTests/OpenTelemetryTests.cs index b642a0e7..5370057e 100644 --- a/test/AWS.Messaging.UnitTests/OpenTelemetryTests.cs +++ b/test/AWS.Messaging.UnitTests/OpenTelemetryTests.cs @@ -49,7 +49,7 @@ public OpenTelemetryTests() var messagePublisherLogger = new Mock>(); var sqsPublisherLogger = new Mock>(); var publisherMapping = new PublisherMapping(typeof(ChatMessage), new SQSPublisherConfiguration("endpoint"), PublisherTargetType.SQS_PUBLISHER); - _subscriberMapping = new SubscriberMapping(typeof(ChatMessageHandler), typeof(ChatMessage)); + _subscriberMapping = SubscriberMapping.Create(); envelopeSerializer.SetReturnsDefault(ValueTask.FromResult(new MessageEnvelope() { diff --git a/test/AWS.Messaging.UnitTests/SerializationTests/MessageSerializerTests.cs b/test/AWS.Messaging.UnitTests/SerializationTests/MessageSerializerTests.cs index 25e8ca5d..8f7015ac 100644 --- a/test/AWS.Messaging.UnitTests/SerializationTests/MessageSerializerTests.cs +++ b/test/AWS.Messaging.UnitTests/SerializationTests/MessageSerializerTests.cs @@ -2,9 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 using System; +using System.Collections; +using System.Collections.Generic; using System.Text.Json.Serialization; using AWS.Messaging.Configuration; using AWS.Messaging.Serialization; +using AWS.Messaging.Services; using AWS.Messaging.UnitTests.Models; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -23,11 +26,12 @@ public MessageSerializerTests() _logger = new Mock>(); } - [Fact] - public void Serialize() + [Theory] + [ClassData(typeof(JsonSerializerContextClassData))] + public void Serialize(IMessageJsonSerializerContextContainer messageJsonSerializerContextFactory) { // ARRANGE - IMessageSerializer serializer = new MessageSerializer(new NullLogger(), new MessageConfiguration()); + IMessageSerializer serializer = new MessageSerializer(new NullLogger(), new MessageConfiguration(), messageJsonSerializerContextFactory); var person = new PersonInfo { FirstName= "Bob", @@ -50,11 +54,12 @@ public void Serialize() Assert.Equal(expectedString, jsonString); } - [Fact] - public void Serialize_NoDataMessageLogging_NoError() + [Theory] + [ClassData(typeof(JsonSerializerContextClassData))] + public void Serialize_NoDataMessageLogging_NoError(IMessageJsonSerializerContextContainer messageJsonSerializerContextFactory) { var messageConfiguration = new MessageConfiguration(); - IMessageSerializer serializer = new MessageSerializer(_logger.Object, messageConfiguration); + IMessageSerializer serializer = new MessageSerializer(_logger.Object, messageConfiguration, messageJsonSerializerContextFactory); var person = new PersonInfo { @@ -81,11 +86,12 @@ public void Serialize_NoDataMessageLogging_NoError() Times.Once); } - [Fact] - public void Serialize_DataMessageLogging_NoError() + [Theory] + [ClassData(typeof(JsonSerializerContextClassData))] + public void Serialize_DataMessageLogging_NoError(IMessageJsonSerializerContextContainer messageJsonSerializerContextFactory) { var messageConfiguration = new MessageConfiguration{ LogMessageContent = true }; - IMessageSerializer serializer = new MessageSerializer(_logger.Object, messageConfiguration); + IMessageSerializer serializer = new MessageSerializer(_logger.Object, messageConfiguration, messageJsonSerializerContextFactory); var person = new PersonInfo { @@ -114,7 +120,7 @@ public void Serialize_DataMessageLogging_NoError() Times.Once); } - private class UnsupportedType + public class UnsupportedType { public string? Name { get; set; } public UnsupportedType? Type { get; set; } @@ -124,7 +130,10 @@ private class UnsupportedType public void Serialize_NoDataMessageLogging_WithError() { var messageConfiguration = new MessageConfiguration(); - IMessageSerializer serializer = new MessageSerializer(_logger.Object, messageConfiguration); + + // This test doesn't use the JsonSerializationContext version because System.Text.Json + // doesn't detect circular references like the reflection version. + IMessageSerializer serializer = new MessageSerializer(_logger.Object, messageConfiguration, new NullMessageJsonSerializerContextContainer()); // Creating an object with circular dependency to force an exception in the JsonSerializer.Serialize method. var unsupportedType1 = new UnsupportedType { Name = "type1" }; @@ -138,11 +147,12 @@ public void Serialize_NoDataMessageLogging_WithError() Assert.Null(exception.InnerException); } - [Fact] - public void Serialize_DataMessageLogging_WithError() + [Theory] + [ClassData(typeof(JsonSerializerContextClassData))] + public void Serialize_DataMessageLogging_WithError(IMessageJsonSerializerContextContainer messageJsonSerializerContextFactory) { var messageConfiguration = new MessageConfiguration{ LogMessageContent = true }; - IMessageSerializer serializer = new MessageSerializer(_logger.Object, messageConfiguration); + IMessageSerializer serializer = new MessageSerializer(_logger.Object, messageConfiguration, messageJsonSerializerContextFactory); // Creating an object with circular dependency to force an exception in the JsonSerializer.Serialize method. var unsupportedType1 = new UnsupportedType { Name = "type1" }; @@ -156,11 +166,12 @@ public void Serialize_DataMessageLogging_WithError() Assert.NotNull(exception.InnerException); } - [Fact] - public void Deserialize() + [Theory] + [ClassData(typeof(JsonSerializerContextClassData))] + public void Deserialize(IMessageJsonSerializerContextContainer messageJsonSerializerContextFactory) { // ARRANGE - IMessageSerializer serializer = new MessageSerializer(new NullLogger(), new MessageConfiguration()); + IMessageSerializer serializer = new MessageSerializer(new NullLogger(), new MessageConfiguration(), messageJsonSerializerContextFactory); var jsonString = @"{ ""FirstName"":""Bob"", @@ -187,11 +198,12 @@ public void Deserialize() Assert.Equal("00001", message.Address?.ZipCode); } - [Fact] - public void Deserialize_NoDataMessageLogging_NoError() + [Theory] + [ClassData(typeof(JsonSerializerContextClassData))] + public void Deserialize_NoDataMessageLogging_NoError(IMessageJsonSerializerContextContainer messageJsonSerializerContextFactory) { var messageConfiguration = new MessageConfiguration(); - IMessageSerializer serializer = new MessageSerializer(_logger.Object, messageConfiguration); + IMessageSerializer serializer = new MessageSerializer(_logger.Object, messageConfiguration, messageJsonSerializerContextFactory); var jsonString = @"{ @@ -217,11 +229,12 @@ public void Deserialize_NoDataMessageLogging_NoError() Times.Once); } - [Fact] - public void Deserialize_DataMessageLogging_NoError() + [Theory] + [ClassData(typeof(JsonSerializerContextClassData))] + public void Deserialize_DataMessageLogging_NoError(IMessageJsonSerializerContextContainer messageJsonSerializerContextFactory) { var messageConfiguration = new MessageConfiguration{ LogMessageContent = true }; - IMessageSerializer serializer = new MessageSerializer(_logger.Object, messageConfiguration); + IMessageSerializer serializer = new MessageSerializer(_logger.Object, messageConfiguration, messageJsonSerializerContextFactory); var jsonString = @"{""FirstName"":""Bob""}"; @@ -238,11 +251,12 @@ public void Deserialize_DataMessageLogging_NoError() Times.Once); } - [Fact] - public void Deserialize_NoDataMessageLogging_WithError() + [Theory] + [ClassData(typeof(JsonSerializerContextClassData))] + public void Deserialize_NoDataMessageLogging_WithError(IMessageJsonSerializerContextContainer messageJsonSerializerContextFactory) { var messageConfiguration = new MessageConfiguration(); - IMessageSerializer serializer = new MessageSerializer(_logger.Object, messageConfiguration); + IMessageSerializer serializer = new MessageSerializer(_logger.Object, messageConfiguration, messageJsonSerializerContextFactory); var jsonString = "{'FirstName':'Bob'}"; @@ -252,11 +266,12 @@ public void Deserialize_NoDataMessageLogging_WithError() Assert.Null(exception.InnerException); } - [Fact] - public void Deserialize_DataMessageLogging_WithError() + [Theory] + [ClassData(typeof(JsonSerializerContextClassData))] + public void Deserialize_DataMessageLogging_WithError(IMessageJsonSerializerContextContainer messageJsonSerializerContextFactory) { var messageConfiguration = new MessageConfiguration{ LogMessageContent = true }; - IMessageSerializer serializer = new MessageSerializer(_logger.Object, messageConfiguration); + IMessageSerializer serializer = new MessageSerializer(_logger.Object, messageConfiguration, messageJsonSerializerContextFactory); var jsonString = "{'FirstName':'Bob'}"; @@ -266,3 +281,21 @@ public void Deserialize_DataMessageLogging_WithError() Assert.NotNull(exception.InnerException); } } + +[JsonSerializable(typeof(PersonInfo))] +[JsonSerializable(typeof(MessageSerializerTests.UnsupportedType))] +[JsonSourceGenerationOptions(UseStringEnumConverter = true)] +public partial class UnitTestsSerializerContext : JsonSerializerContext +{ +} + +public class JsonSerializerContextClassData : IEnumerable +{ + public IEnumerator GetEnumerator() + { + yield return new object[] { new NullMessageJsonSerializerContextContainer() }; + yield return new object[] { new DefaultMessageJsonSerializerContextContainer(UnitTestsSerializerContext.Default) }; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/test/AWS.Messaging.UnitTests/SubscriberMappingTests.cs b/test/AWS.Messaging.UnitTests/SubscriberMappingTests.cs index bf763930..736b9087 100644 --- a/test/AWS.Messaging.UnitTests/SubscriberMappingTests.cs +++ b/test/AWS.Messaging.UnitTests/SubscriberMappingTests.cs @@ -13,14 +13,14 @@ public class SubscriberMappingTests [Fact] public void SubscriberMappingNoMessageIdentifier() { - var mapping = new SubscriberMapping(typeof(ChatMessageHandler), typeof(ChatMessage)); + var mapping = SubscriberMapping.Create(); Assert.Equal("AWS.Messaging.UnitTests.Models.ChatMessage", mapping.MessageTypeIdentifier); } [Fact] public void SubscriberMappingWithMessageIdentifier() { - var mapping = new SubscriberMapping(typeof(ChatMessageHandler), typeof(ChatMessage), "CustomIdentifier"); + var mapping = SubscriberMapping.Create("CustomIdentifier"); Assert.Equal("CustomIdentifier", mapping.MessageTypeIdentifier); } }