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

Updates the MessageSerializer to store the json value as the actual json object in the data field. #190

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .autover/changes/56a61b2c-4302-4077-9dc6-3fc3bc60a998.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Projects": [
{
"Name": "AWS.Messaging",
"Type": "Patch",
"ChangelogMessages": [
"Update MessageSerializer to store data as actual json. Fixes #168"
]
}
]
}
3 changes: 2 additions & 1 deletion src/AWS.Messaging/Exceptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using System.Text.Json;
using AWS.Messaging.Configuration;
using AWS.Messaging.Publishers.EventBridge;
using AWS.Messaging.Serialization;
Expand Down Expand Up @@ -97,7 +98,7 @@ public InvalidSubscriberEndpointException(string message, Exception? innerExcept
}

/// <summary>
/// Thrown if failed to deserialize the application message while invoking <see cref="IMessageSerializer.Deserialize(string, Type)"/>
/// Thrown if failed to deserialize the application message while invoking <see cref="IMessageSerializer.Deserialize(JsonElement,Type)"/>
/// </summary>
public class FailedToDeserializeApplicationMessageException : AWSMessagingException
{
Expand Down
6 changes: 6 additions & 0 deletions src/AWS.Messaging/MessageEnvelope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ public abstract class MessageEnvelope
[JsonPropertyName("time")]
public DateTimeOffset TimeStamp { get; set; } = DateTimeOffset.MinValue;

/// <summary>
/// The data content type.
/// </summary>
[JsonPropertyName("datacontenttype")]
public string? DataContentType { get; set; } = null!;

/// <summary>
/// This stores different metadata that is not modeled as a top-level property in MessageEnvelope class.
/// These entries will also be serialized as top-level properties when sending the message, which
Expand Down
4 changes: 3 additions & 1 deletion src/AWS.Messaging/Serialization/EnvelopeSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ public async ValueTask<string> SerializeAsync<T>(MessageEnvelope<T> envelope)
["specversion"] = envelope.Version,
["type"] = envelope.MessageTypeIdentifier,
["time"] = envelope.TimeStamp,
["datacontenttype"] = _messageSerializer.DataContentType,
["data"] = _messageSerializer.Serialize(message)
};

Expand Down Expand Up @@ -182,6 +183,7 @@ private MessageEnvelope DeserializeEnvelope(string envelopeString, Type messageT
envelope.Version = JsonPropertyHelper.GetRequiredProperty(root, "specversion", element => element.GetString()!);
envelope.MessageTypeIdentifier = JsonPropertyHelper.GetRequiredProperty(root, "type", element => element.GetString()!);
envelope.TimeStamp = JsonPropertyHelper.GetRequiredProperty(root, "time", element => element.GetDateTimeOffset());
envelope.DataContentType = JsonPropertyHelper.GetStringProperty(root, "datacontenttype");

// Handle metadata if present
if (root.TryGetProperty("metadata", out var metadataElement))
Expand All @@ -190,7 +192,7 @@ private MessageEnvelope DeserializeEnvelope(string envelopeString, Type messageT
}

// Deserialize the message content using the custom serializer
var dataContent = JsonPropertyHelper.GetRequiredProperty(root, "data", element => element.GetString()!);
var dataContent = JsonPropertyHelper.GetRequiredProperty(root, "data", element => element);
var message = _messageSerializer.Deserialize(dataContent, messageType);
envelope.SetMessage(message);

Expand Down
29 changes: 15 additions & 14 deletions src/AWS.Messaging/Serialization/IMessageSerializer.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using System.Text.Json;

namespace AWS.Messaging.Serialization;

/// <summary>
Expand All @@ -9,25 +11,24 @@ namespace AWS.Messaging.Serialization;
public interface IMessageSerializer
{
/// <summary>
/// Serializes the .NET message object into a string.
/// Serializes a .NET object into a format suitable for message transport.
/// The specific return type depends on the implementation, but must be compatible
/// with message transport requirements (e.g., JsonNode for CloudEvents).
/// </summary>
/// <param name="message">The .NET object that will be serialized.</param>
string Serialize(object message);
/// <param name="message">The .NET object to be serialized.</param>
/// <returns>A serialized representation of the message in a format appropriate for the messaging system.</returns>
dynamic Serialize(object message);

/// <summary>
/// Deserializes the raw string message into the .NET type.
/// Deserializes a JsonElement message into the specified .NET type.
/// </summary>
/// <param name="message">The string message that will be deserialized.</param>
/// <param name="deserializedType">The .NET type that represents the deserialized message.</param>
object Deserialize(string message, Type deserializedType);
/// <param name="message">The JsonElement containing the message to be deserialized.</param>
/// <param name="deserializedType">The target .NET type for deserialization.</param>
/// <returns>An instance of the specified type containing the deserialized data.</returns>
object Deserialize(JsonElement message, Type deserializedType);

/// <summary>
/// Deserializes the raw string message into the .NET type.
/// Gets the content type of the serialized data.
/// </summary>
/// <typeparam name="T">The .NET type that represents the deserialized message.</typeparam>
/// <param name="message">The string message that will be deserialized.</param>
T Deserialize<T>(string message)
{
return (T)Deserialize(message, typeof(T));
}
string DataContentType { get; }
}
50 changes: 37 additions & 13 deletions src/AWS.Messaging/Serialization/MessageSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,18 @@ public MessageSerializer(ILogger<MessageSerializer> logger, IMessageConfiguratio
_messageConfiguration= messageConfiguration;
}

/// <inheritdoc/>
/// <exception cref="FailedToDeserializeApplicationMessageException"></exception>
public object Deserialize(string message, Type deserializedType)
/// <summary>
/// Deserializes a JsonElement message into the specified type.
/// </summary>
/// <param name="message">The JsonElement containing the message to deserialize.</param>
/// <param name="deserializedType">The target Type to deserialize the message into.</param>
/// <returns>An object of the specified deserializedType containing the deserialized message data.</returns>
/// <exception cref="FailedToDeserializeApplicationMessageException">Thrown when deserialization fails.</exception>
/// <remarks>
/// Uses System.Text.Json for deserialization with configuration options from IMessageConfiguration.
/// Logging behavior is controlled by the LogMessageContent configuration setting.
/// </remarks>
public object Deserialize(JsonElement message, Type deserializedType)
{
try
{
Expand Down Expand Up @@ -52,24 +61,37 @@ public object Deserialize(string message, Type deserializedType)
}
}

/// <inheritdoc/>
/// <exception cref="FailedToSerializeApplicationMessageException"></exception>
public string Serialize(object message)
/// <summary>
/// Serializes an object into a JsonNode, maintaining the data in its JSON object form
/// to align with CloudEvents specification for data content.
/// </summary>
/// <param name="message">The object to serialize.</param>
/// <returns>A JsonNode representing the serialized message, preserving the JSON structure
/// for direct use in CloudEvents data field.</returns>
/// <exception cref="FailedToSerializeApplicationMessageException">Thrown when serialization fails.</exception>
/// <remarks>
/// Uses System.Text.Json for serialization with configuration options from IMessageConfiguration.
/// Returns a JsonNode instead of a string to maintain the JSON structure, which is optimal for
/// CloudEvents integration where the data field expects structured JSON content.
/// Logging behavior is controlled by the LogMessageContent configuration setting.
/// </remarks>
public dynamic Serialize(object message)
{
if (message == null)
{
throw new FailedToSerializeApplicationMessageException("Cannot serialize null object");
}

try
{
var jsonSerializerOptions = _messageConfiguration.SerializationOptions.SystemTextJsonOptions;
var jsonString = JsonSerializer.Serialize(message, jsonSerializerOptions);
var jsonNode = JsonSerializer.SerializeToNode(message, jsonSerializerOptions);
if (_messageConfiguration.LogMessageContent)
{
_logger.LogTrace("Serialized the message object as the following raw string:\n{JsonString}", jsonString);
}
else
{
_logger.LogTrace("Serialized the message object to a raw string with a content length of {ContentLength}.", jsonString.Length);
_logger.LogTrace("Serialized the message object as the following :\n{JsonString}", jsonNode);
}

return jsonString;
return jsonNode!;
}
catch (JsonException) when (!_messageConfiguration.LogMessageContent)
{
Expand All @@ -82,4 +104,6 @@ public string Serialize(object message)
throw new FailedToSerializeApplicationMessageException("Failed to serialize application message into a string", ex);
}
}

public string DataContentType => "application/json";
}
25 changes: 14 additions & 11 deletions test/AWS.Messaging.IntegrationTests/EventBridgePublisherTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
using Amazon.SQS.Model;
using Amazon.SecurityToken;
using Amazon.SecurityToken.Model;
using AWS.Messaging.IntegrationTests.Handlers;
using AWS.Messaging.Serialization;

namespace AWS.Messaging.IntegrationTests;

Expand Down Expand Up @@ -105,6 +107,7 @@ await _eventBridgeClient.PutTargetsAsync(new PutTargetsRequest
{
builder.AddEventBridgePublisher<ChatMessage>(_eventBusArn);
builder.AddMessageSource("/aws/messaging");
builder.AddMessageHandler<ChatMessageHandler, ChatMessage>();
});
_serviceProvider = serviceCollection.BuildServiceProvider();
}
Expand All @@ -126,23 +129,23 @@ await publisher.PublishAsync(new ChatMessage
var receiveMessageResponse = await _sqsClient.ReceiveMessageAsync(_sqsQueueUrl);
var message = Assert.Single(receiveMessageResponse.Messages);

// EventBridge adds an external envelope which we need to strip away
var eventBridgeEnvelope = JsonSerializer.Deserialize<EventBridgeEnvelope>(message.Body);
Assert.NotNull(eventBridgeEnvelope);
var envelopeSerializer = _serviceProvider.GetRequiredService<IEnvelopeSerializer>();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these had to be updated because the previously used JsonSerializer.Deseerialize which doesnt work anymore with new format


Assert.NotNull(eventBridgeEnvelope.Detail);
var envelope = eventBridgeEnvelope.Detail;
// Use the EnvelopeSerializer to convert the message
var result = await envelopeSerializer.ConvertToEnvelopeAsync(message);
var envelope = result.Envelope as MessageEnvelope<ChatMessage>;

Assert.NotNull(envelope);
Assert.False(string.IsNullOrEmpty(envelope.Id));
Assert.Equal("/aws/messaging", envelope.Source.ToString());
Assert.True(envelope.TimeStamp > publishStartTime);
Assert.True(envelope.TimeStamp < publishEndTime);
Assert.Equal(typeof(ChatMessage).ToString(), envelope.MessageTypeIdentifier);

var messageType = Type.GetType(eventBridgeEnvelope.Detail.MessageTypeIdentifier);
Assert.NotNull(messageType);

var chatMessageObject = JsonSerializer.Deserialize(eventBridgeEnvelope.Detail.Message, messageType);
var chatMessage = Assert.IsType<ChatMessage>(chatMessageObject);
Assert.Equal("Test1", chatMessage.MessageDescription);
var chatMessage = envelope.Message;
Assert.NotNull(chatMessage);
Assert.IsType<ChatMessage>(chatMessage);
Assert.Equal("Test1", chatMessage.MessageDescription);;
}

public async Task DisposeAsync()
Expand Down
20 changes: 14 additions & 6 deletions test/AWS.Messaging.IntegrationTests/SNSPublisherTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
using System.Text.Json;
using Amazon.SimpleNotificationService;
using Amazon.SimpleNotificationService.Model;
using AWS.Messaging.IntegrationTests.Handlers;
using AWS.Messaging.Serialization;

namespace AWS.Messaging.IntegrationTests;

Expand Down Expand Up @@ -45,6 +47,7 @@ public async Task InitializeAsync()
{
builder.AddSNSPublisher<ChatMessage>(_snsTopicArn);
builder.AddMessageSource("/aws/messaging");
builder.AddMessageHandler<ChatMessageHandler, ChatMessage>();
});
_serviceProvider = serviceCollection.BuildServiceProvider();
}
Expand All @@ -70,18 +73,23 @@ await publisher.PublishAsync(new ChatMessage
var snsEnvelope = JsonSerializer.Deserialize<SNSEnvelope>(message.Body);
Assert.NotNull(snsEnvelope);

var envelope = JsonSerializer.Deserialize<MessageEnvelope<string>>(snsEnvelope.Message);
// Get the EnvelopeSerializer from the service provider
var envelopeSerializer = _serviceProvider.GetRequiredService<IEnvelopeSerializer>();

// Use the EnvelopeSerializer to convert the message
var result = await envelopeSerializer.ConvertToEnvelopeAsync(message);
var envelope = result.Envelope as MessageEnvelope<ChatMessage>;

Assert.NotNull(envelope);
Assert.False(string.IsNullOrEmpty(envelope.Id));
Assert.Equal("/aws/messaging", envelope.Source.ToString());
Assert.True(envelope.TimeStamp > publishStartTime);
Assert.True(envelope.TimeStamp < publishEndTime);
Assert.Equal(typeof(ChatMessage).ToString(), envelope.MessageTypeIdentifier);

var messageType = Type.GetType(envelope.MessageTypeIdentifier);
Assert.NotNull(messageType);

var chatMessageObject = JsonSerializer.Deserialize(envelope.Message, messageType);
var chatMessage = Assert.IsType<ChatMessage>(chatMessageObject);
var chatMessage = envelope.Message;
Assert.NotNull(chatMessage);
Assert.IsType<ChatMessage>(chatMessage);
Assert.Equal("Test1", chatMessage.MessageDescription);
}

Expand Down
24 changes: 18 additions & 6 deletions test/AWS.Messaging.IntegrationTests/SQSPublisherTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
using Microsoft.Extensions.DependencyInjection;
using AWS.Messaging.IntegrationTests.Models;
using System.Text.Json;
using AWS.Messaging.IntegrationTests.Handlers;
using AWS.Messaging.Serialization;

namespace AWS.Messaging.IntegrationTests;

Expand Down Expand Up @@ -33,6 +35,8 @@ public async Task InitializeAsync()
{
builder.AddSQSPublisher<ChatMessage>(_sqsQueueUrl);
builder.AddMessageSource("/aws/messaging");
builder.AddMessageHandler<ChatMessageHandler, ChatMessage>();

});
_serviceProvider = serviceCollection.BuildServiceProvider();
}
Expand All @@ -54,19 +58,27 @@ await publisher.PublishAsync(new ChatMessage
var receiveMessageResponse = await _sqsClient.ReceiveMessageAsync(_sqsQueueUrl);
var message = Assert.Single(receiveMessageResponse.Messages);

var envelope = JsonSerializer.Deserialize<MessageEnvelope<string>>(message.Body);
// Get the EnvelopeSerializer from the service provider
var envelopeSerializer = _serviceProvider.GetRequiredService<IEnvelopeSerializer>();

// Use the EnvelopeSerializer to convert the message
var result = await envelopeSerializer.ConvertToEnvelopeAsync(message);
var envelope = result.Envelope as MessageEnvelope<ChatMessage>;

Assert.NotNull(envelope);
Assert.False(string.IsNullOrEmpty(envelope.Id));
Assert.Equal("/aws/messaging", envelope.Source.ToString());
Assert.True(envelope.TimeStamp > publishStartTime);
Assert.True(envelope.TimeStamp > publishStartTime);
Assert.True(envelope.TimeStamp < publishEndTime);
var messageType = Type.GetType(envelope.MessageTypeIdentifier);
Assert.NotNull(messageType);
var chatMessageObject = JsonSerializer.Deserialize(envelope.Message, messageType);
var chatMessage = Assert.IsType<ChatMessage>(chatMessageObject);
Assert.Equal(typeof(ChatMessage).ToString(), envelope.MessageTypeIdentifier);

var chatMessage = envelope.Message;
Assert.NotNull(chatMessage);
Assert.IsType<ChatMessage>(chatMessage);
Assert.Equal("Test1", chatMessage.MessageDescription);
}


public async Task DisposeAsync()
{
try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@

// ASSERT
// The \u0022 corresponds to quotation mark (")
var expectedBlob = "{\"id\":\"id-123\",\"source\":\"/backend/service\",\"specversion\":\"1.0\",\"type\":\"addressInfo\",\"time\":\"2000-12-05T10:30:55+00:00\",\"data\":\"{\\u0022Unit\\u0022:123,\\u0022Street\\u0022:\\u0022Prince St\\u0022,\\u0022ZipCode\\u0022:\\u002200001\\u0022}\"}";
var expectedBlob = "{\"id\":\"id-123\",\"source\":\"/backend/service\",\"specversion\":\"1.0\",\"type\":\"addressInfo\",\"time\":\"2000-12-05T10:30:55+00:00\",\"datacontenttype\":\"application/json\",\"data\":{\"Unit\":123,\"Street\":\"Prince St\",\"ZipCode\":\"00001\"}}";
Assert.Equal(expectedBlob, jsonBlob);
}

Expand Down Expand Up @@ -270,19 +270,19 @@
var serviceProvider = _serviceCollection.BuildServiceProvider();
var envelopeSerializer = serviceProvider.GetRequiredService<IEnvelopeSerializer>();

var innerMessageEnvelope = new MessageEnvelope<string>
var innerMessageEnvelope = new MessageEnvelope<AddressInfo>
{
Id = "66659d05-e4ff-462f-81c4-09e560e66a5c",
Source = new Uri("/aws/messaging", UriKind.Relative),
Version = "1.0",
MessageTypeIdentifier = "addressInfo",
TimeStamp = _testdate,
Message = JsonSerializer.Serialize(new AddressInfo
Message = new AddressInfo
{
Street = "Prince St",
Unit = 123,
ZipCode = "00001"
})
}
};

var outerMessageEnvelope = new Dictionary<string, object>
Expand Down Expand Up @@ -395,7 +395,7 @@
var serializedMessage = await envelopeSerializer.SerializeAsync(messageEnvelope);

// ASSERT - Check expected base 64 encoded string
var expectedserializedMessage = "eyJpZCI6IjEyMyIsInNvdXJjZSI6Ii9hd3MvbWVzc2FnaW5nIiwic3BlY3ZlcnNpb24iOiIxLjAiLCJ0eXBlIjoiYWRkcmVzc0luZm8iLCJ0aW1lIjoiMjAwMC0xMi0wNVQxMDozMDo1NSswMDowMCIsImRhdGEiOiJ7XHUwMDIyVW5pdFx1MDAyMjoxMjMsXHUwMDIyU3RyZWV0XHUwMDIyOlx1MDAyMlByaW5jZSBTdFx1MDAyMixcdTAwMjJaaXBDb2RlXHUwMDIyOlx1MDAyMjAwMDAxXHUwMDIyfSIsIklzLURlbGl2ZXJlZCI6ZmFsc2V9";
var expectedserializedMessage = "eyJpZCI6IjEyMyIsInNvdXJjZSI6Ii9hd3MvbWVzc2FnaW5nIiwic3BlY3ZlcnNpb24iOiIxLjAiLCJ0eXBlIjoiYWRkcmVzc0luZm8iLCJ0aW1lIjoiMjAwMC0xMi0wNVQxMDozMDo1NSswMDowMCIsImRhdGFjb250ZW50dHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJkYXRhIjp7IlVuaXQiOjEyMywiU3RyZWV0IjoiUHJpbmNlIFN0IiwiWmlwQ29kZSI6IjAwMDAxIn0sIklzLURlbGl2ZXJlZCI6ZmFsc2V9";
Assert.Equal(expectedserializedMessage, serializedMessage);

// ACT - Convert To Envelope from base 64 Encoded Message
Expand Down Expand Up @@ -457,7 +457,7 @@
logger.Verify(log => log.Log(
It.Is<LogLevel>(logLevel => logLevel == LogLevel.Trace),
It.Is<EventId>(eventId => eventId.Id == 0),
It.Is<It.IsAnyType>((@object, @type) => @object.ToString() == "Serialized the MessageEnvelope object as the following raw string:\n{\"id\":\"123\",\"source\":\"/aws/messaging\",\"specversion\":\"1.0\",\"type\":\"addressInfo\",\"time\":\"2000-12-05T10:30:55+00:00\",\"data\":null}"),
It.Is<It.IsAnyType>((@object, @type) => @object.ToString() == "Serialized the MessageEnvelope object as the following raw string:\n{\"id\":\"123\",\"source\":\"/aws/messaging\",\"specversion\":\"1.0\",\"type\":\"addressInfo\",\"time\":\"2000-12-05T10:30:55+00:00\",\"datacontenttype\":null,\"data\":null}"),
null,
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
Expand Down Expand Up @@ -640,7 +640,7 @@
// Verify the inner exception type and message
Assert.IsType<InvalidDataException>(exception.InnerException);
var innerException = exception.InnerException as InvalidDataException;
Assert.Contains("'unknownMessageType' is not a valid subscriber mapping.", innerException.Message);

Check warning on line 643 in test/AWS.Messaging.UnitTests/SerializationTests/EnvelopeSerializerTests.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 643 in test/AWS.Messaging.UnitTests/SerializationTests/EnvelopeSerializerTests.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
Assert.Contains("Available mappings:", innerException.Message);
Assert.Contains("addressInfo", innerException.Message);
}
Expand Down
Loading
Loading