diff --git a/AyCode.Core.Tests/Serialization/AcBinaryDateTimeSerializationTests.cs b/AyCode.Core.Tests/Serialization/AcBinaryDateTimeSerializationTests.cs
new file mode 100644
index 0000000..0d89f4e
--- /dev/null
+++ b/AyCode.Core.Tests/Serialization/AcBinaryDateTimeSerializationTests.cs
@@ -0,0 +1,473 @@
+using AyCode.Core.Extensions;
+
+namespace AyCode.Core.Tests.Serialization;
+
+///
+/// Tests for DateTime serialization in Binary format.
+/// Covers edge cases like string-stored DateTime values in GenericAttribute-like scenarios.
+///
+[TestClass]
+public class AcBinaryDateTimeSerializationTests
+{
+ #region DateTime Direct Serialization
+
+ [TestMethod]
+ public void DateTime_RoundTrip_PreservesValue()
+ {
+ var original = new DateTime(2025, 10, 24, 0, 27, 0, DateTimeKind.Utc);
+
+ var binary = AcBinarySerializer.Serialize(original);
+ var result = binary.BinaryTo();
+
+ Assert.AreEqual(original, result);
+ Assert.AreEqual(DateTimeKind.Utc, result.Kind);
+ }
+
+ [TestMethod]
+ public void NullableDateTime_RoundTrip_PreservesValue()
+ {
+ DateTime? original = new DateTime(2025, 10, 24, 0, 27, 0, DateTimeKind.Utc);
+
+ var binary = AcBinarySerializer.Serialize(original);
+ var result = binary.BinaryTo();
+
+ Assert.AreEqual(original, result);
+ }
+
+ [TestMethod]
+ public void NullableDateTime_Null_RoundTrip_PreservesNull()
+ {
+ DateTime? original = null;
+
+ var binary = AcBinarySerializer.Serialize(original);
+ var result = binary.BinaryTo();
+
+ Assert.IsNull(result);
+ }
+
+ #endregion
+
+ #region Object with DateTime Property
+
+ [TestMethod]
+ public void ObjectWithDateTime_RoundTrip_PreservesValue()
+ {
+ var original = new TestObjectWithDateTime
+ {
+ Id = 1,
+ CreatedAt = new DateTime(2025, 10, 24, 0, 27, 0, DateTimeKind.Utc),
+ UpdatedAt = new DateTime(2025, 12, 31, 23, 59, 59, DateTimeKind.Utc)
+ };
+
+ var binary = original.ToBinary();
+ var result = binary.BinaryTo();
+
+ Assert.IsNotNull(result);
+ Assert.AreEqual(original.Id, result.Id);
+ Assert.AreEqual(original.CreatedAt, result.CreatedAt);
+ Assert.AreEqual(original.UpdatedAt, result.UpdatedAt);
+ }
+
+ [TestMethod]
+ public void ObjectWithNullableDateTime_Null_RoundTrip_PreservesNull()
+ {
+ var original = new TestObjectWithNullableDateTime
+ {
+ Id = 1,
+ DateOfReceipt = null
+ };
+
+ var binary = original.ToBinary();
+ var result = binary.BinaryTo();
+
+ Assert.IsNotNull(result);
+ Assert.AreEqual(original.Id, result.Id);
+ Assert.IsNull(result.DateOfReceipt);
+ }
+
+ [TestMethod]
+ public void ObjectWithNullableDateTime_HasValue_RoundTrip_PreservesValue()
+ {
+ var original = new TestObjectWithNullableDateTime
+ {
+ Id = 1,
+ DateOfReceipt = new DateTime(2025, 10, 24, 0, 27, 0, DateTimeKind.Utc)
+ };
+
+ var binary = original.ToBinary();
+ var result = binary.BinaryTo();
+
+ Assert.IsNotNull(result);
+ Assert.AreEqual(original.Id, result.Id);
+ Assert.AreEqual(original.DateOfReceipt, result.DateOfReceipt);
+ }
+
+ #endregion
+
+ #region GenericAttribute-like Scenario (String stored DateTime)
+
+ [TestMethod]
+ public void GenericAttributeScenario_DateTimeAsString_PreservesValue()
+ {
+ var original = new TestGenericAttribute
+ {
+ Key = "DateOfReceipt",
+ Value = "10/24/2025 00:27:00"
+ };
+
+ var binary = original.ToBinary();
+ var result = binary.BinaryTo();
+
+ Assert.IsNotNull(result);
+ Assert.AreEqual(original.Key, result.Key);
+ Assert.AreEqual(original.Value, result.Value);
+ }
+
+ [TestMethod]
+ public void GenericAttributeScenario_ZeroValue_PreservesValue()
+ {
+ var original = new TestGenericAttribute
+ {
+ Key = "DateOfReceipt",
+ Value = "0"
+ };
+
+ var binary = original.ToBinary();
+ var result = binary.BinaryTo();
+
+ Assert.IsNotNull(result);
+ Assert.AreEqual(original.Key, result.Key);
+ Assert.AreEqual("0", result.Value);
+ }
+
+ [TestMethod]
+ public void GenericAttributeList_RoundTrip_PreservesAllValues()
+ {
+ var original = new List
+ {
+ new() { Key = "DateOfReceipt", Value = "10/24/2025 00:27:00" },
+ new() { Key = "SomeNumber", Value = "42" },
+ new() { Key = "EmptyValue", Value = "" },
+ new() { Key = "ZeroValue", Value = "0" }
+ };
+
+ var binary = original.ToBinary();
+ var result = binary.BinaryTo>();
+
+ Assert.IsNotNull(result);
+ Assert.AreEqual(4, result.Count);
+
+ Assert.AreEqual("DateOfReceipt", result[0].Key);
+ Assert.AreEqual("10/24/2025 00:27:00", result[0].Value);
+
+ Assert.AreEqual("SomeNumber", result[1].Key);
+ Assert.AreEqual("42", result[1].Value);
+
+ Assert.AreEqual("EmptyValue", result[2].Key);
+ Assert.AreEqual("", result[2].Value);
+
+ Assert.AreEqual("ZeroValue", result[3].Key);
+ Assert.AreEqual("0", result[3].Value);
+ }
+
+ [TestMethod]
+ public void ObjectWithGenericAttributes_RoundTrip_PreservesAllValues()
+ {
+ var original = new TestDtoWithGenericAttributes
+ {
+ Id = 123,
+ Name = "Test Order",
+ GenericAttributes =
+ [
+ new() { Key = "DateOfReceipt", Value = "10/24/2025 00:27:00" },
+ new() { Key = "Priority", Value = "1" }
+ ]
+ };
+
+ var binary = original.ToBinary();
+ var result = binary.BinaryTo();
+
+ Assert.IsNotNull(result);
+ Assert.AreEqual(123, result.Id);
+ Assert.AreEqual("Test Order", result.Name);
+ Assert.AreEqual(2, result.GenericAttributes.Count);
+
+ var dateAttr = result.GenericAttributes.FirstOrDefault(x => x.Key == "DateOfReceipt");
+ Assert.IsNotNull(dateAttr);
+ Assert.AreEqual("10/24/2025 00:27:00", dateAttr.Value);
+ }
+
+ #endregion
+
+ #region JSON vs Binary Comparison
+
+ [TestMethod]
+ public void GenericAttribute_JsonAndBinary_ProduceSameResult()
+ {
+ var original = new TestGenericAttribute
+ {
+ Key = "DateOfReceipt",
+ Value = "10/24/2025 00:27:00"
+ };
+
+ var json = original.ToJson();
+ var jsonResult = json.JsonTo();
+
+ var binary = original.ToBinary();
+ var binaryResult = binary.BinaryTo();
+
+ Assert.IsNotNull(jsonResult);
+ Assert.IsNotNull(binaryResult);
+
+ Assert.AreEqual(jsonResult.Key, binaryResult.Key);
+ Assert.AreEqual(jsonResult.Value, binaryResult.Value);
+ }
+
+ [TestMethod]
+ public void DtoWithGenericAttributes_JsonAndBinary_ProduceSameResult()
+ {
+ var original = new TestDtoWithGenericAttributes
+ {
+ Id = 123,
+ Name = "Test Order",
+ GenericAttributes =
+ [
+ new() { Key = "DateOfReceipt", Value = "10/24/2025 00:27:00" },
+ new() { Key = "ZeroValue", Value = "0" }
+ ]
+ };
+
+ var json = original.ToJson();
+ var jsonResult = json.JsonTo();
+
+ var binary = original.ToBinary();
+ var binaryResult = binary.BinaryTo();
+
+ Assert.IsNotNull(jsonResult);
+ Assert.IsNotNull(binaryResult);
+
+ Assert.AreEqual(jsonResult.Id, binaryResult.Id);
+ Assert.AreEqual(jsonResult.Name, binaryResult.Name);
+ Assert.AreEqual(jsonResult.GenericAttributes.Count, binaryResult.GenericAttributes.Count);
+
+ for (int i = 0; i < jsonResult.GenericAttributes.Count; i++)
+ {
+ Assert.AreEqual(jsonResult.GenericAttributes[i].Key, binaryResult.GenericAttributes[i].Key);
+ Assert.AreEqual(jsonResult.GenericAttributes[i].Value, binaryResult.GenericAttributes[i].Value);
+ }
+ }
+
+ #endregion
+
+ #region Test Models
+
+ public class TestObjectWithDateTime
+ {
+ public int Id { get; set; }
+ public DateTime CreatedAt { get; set; }
+ public DateTime UpdatedAt { get; set; }
+ }
+
+ public class TestObjectWithNullableDateTime
+ {
+ public int Id { get; set; }
+ public DateTime? DateOfReceipt { get; set; }
+ }
+
+ public class TestGenericAttribute
+ {
+ public string Key { get; set; } = "";
+ public string Value { get; set; } = "";
+ }
+
+ public class TestDtoWithGenericAttributes
+ {
+ public int Id { get; set; }
+ public string Name { get; set; } = "";
+ public List GenericAttributes { get; set; } = [];
+ }
+
+ #endregion
+
+ #region Exact Production Scenario Test
+
+ ///
+ /// This test reproduces the exact production bug scenario:
+ /// 1. Server sends Binary serialized response with GenericAttributes
+ /// 2. Client deserializes the Binary response
+ /// 3. Client accesses DateOfReceipt property which reads from GenericAttributes
+ /// 4. CommonHelper2.To fails to parse the string value
+ ///
+ [TestMethod]
+ public void ProductionScenario_GenericAttributeWithDateString_PreservesExactFormat()
+ {
+ // Arrange: Create DTO with GenericAttributes like in production
+ var original = new TestDtoWithGenericAttributes
+ {
+ Id = 123,
+ Name = "Test Order",
+ GenericAttributes =
+ [
+ new() { Key = "DateOfReceipt", Value = "10/24/2025 00:27:00" },
+ new() { Key = "Priority", Value = "1" },
+ new() { Key = "SomeFlag", Value = "true" }
+ ]
+ };
+
+ // Act: Binary round-trip (simulates server->client communication)
+ var binary = original.ToBinary();
+ var result = binary.BinaryTo();
+
+ // Assert: The exact string value must be preserved
+ Assert.IsNotNull(result);
+ var dateAttr = result.GenericAttributes.FirstOrDefault(x => x.Key == "DateOfReceipt");
+ Assert.IsNotNull(dateAttr, "DateOfReceipt attribute should exist");
+
+ // This is the critical assertion - the EXACT string must be preserved
+ Assert.AreEqual("10/24/2025 00:27:00", dateAttr.Value,
+ $"Expected '10/24/2025 00:27:00' but got '{dateAttr.Value}'");
+
+ // Verify it can be parsed with US culture (which is how it was stored)
+ Assert.IsTrue(DateTime.TryParse(dateAttr.Value, new System.Globalization.CultureInfo("en-US"),
+ System.Globalization.DateTimeStyles.None, out var parsedDate),
+ $"Value '{dateAttr.Value}' should be parseable as US date format");
+
+ Assert.AreEqual(new DateTime(2025, 10, 24, 0, 27, 0), parsedDate);
+ }
+
+ ///
+ /// Test that verifies the exact bytes of the string are preserved.
+ ///
+ [TestMethod]
+ public void ProductionScenario_StringWithSlashes_BytesArePreserved()
+ {
+ var original = "10/24/2025 00:27:00";
+ var originalBytes = System.Text.Encoding.UTF8.GetBytes(original);
+
+ // Serialize and deserialize
+ var binary = AcBinarySerializer.Serialize(original);
+ var result = binary.BinaryTo();
+
+ Assert.IsNotNull(result);
+ var resultBytes = System.Text.Encoding.UTF8.GetBytes(result);
+
+ // Compare byte-by-byte
+ Assert.AreEqual(originalBytes.Length, resultBytes.Length, "String length changed after serialization");
+ for (int i = 0; i < originalBytes.Length; i++)
+ {
+ Assert.AreEqual(originalBytes[i], resultBytes[i],
+ $"Byte at position {i} differs: expected {originalBytes[i]:X2} ('{(char)originalBytes[i]}'), got {resultBytes[i]:X2} ('{(char)resultBytes[i]}')");
+ }
+ }
+
+ ///
+ /// Test with large list of GenericAttributes to catch any edge cases.
+ ///
+ [TestMethod]
+ public void ProductionScenario_ManyGenericAttributes_AllPreserved()
+ {
+ var original = new TestDtoWithGenericAttributes
+ {
+ Id = 999,
+ Name = "Large Order",
+ GenericAttributes = Enumerable.Range(0, 50).Select(i => new TestGenericAttribute
+ {
+ Key = $"Attribute_{i}",
+ Value = i % 5 == 0 ? $"{i}/24/2025 00:00:00" : i.ToString()
+ }).ToList()
+ };
+
+ var binary = original.ToBinary();
+ var result = binary.BinaryTo();
+
+ Assert.IsNotNull(result);
+ Assert.AreEqual(50, result.GenericAttributes.Count);
+
+ for (int i = 0; i < 50; i++)
+ {
+ var expectedValue = i % 5 == 0 ? $"{i}/24/2025 00:00:00" : i.ToString();
+ Assert.AreEqual($"Attribute_{i}", result.GenericAttributes[i].Key);
+ Assert.AreEqual(expectedValue, result.GenericAttributes[i].Value,
+ $"Attribute_{i} value mismatch: expected '{expectedValue}', got '{result.GenericAttributes[i].Value}'");
+ }
+ }
+
+ #endregion
+
+ #region CommonHelper2.To Simulation Tests
+
+ ///
+ /// This test simulates what CommonHelper2.To does with various string values.
+ /// It helps identify which values will cause the FormatException.
+ ///
+ [TestMethod]
+ public void CommonHelperSimulation_ValidDateString_ParsesSuccessfully()
+ {
+ var dateString = "10/24/2025 00:27:00";
+
+ // This is what CommonHelper2.To does internally
+ var converter = System.ComponentModel.TypeDescriptor.GetConverter(typeof(DateTime));
+ Assert.IsTrue(converter.CanConvertFrom(typeof(string)));
+
+ // With InvariantCulture
+ var result = converter.ConvertFrom(null, System.Globalization.CultureInfo.InvariantCulture, dateString);
+ Assert.IsNotNull(result);
+ Assert.IsInstanceOfType(result, typeof(DateTime));
+ var dt = (DateTime)result;
+ // InvariantCulture interprets 10/24/2025 as October 24, 2025
+ Assert.AreEqual(2025, dt.Year);
+ Assert.AreEqual(10, dt.Month);
+ Assert.AreEqual(24, dt.Day);
+ }
+
+ ///
+ /// This test shows that "0" cannot be parsed as DateTime - this is the actual bug!
+ ///
+ [TestMethod]
+ public void CommonHelperSimulation_ZeroString_ThrowsFormatException()
+ {
+ var invalidValue = "0";
+
+ var converter = System.ComponentModel.TypeDescriptor.GetConverter(typeof(DateTime));
+
+ // This should throw FormatException - exactly what we see in production
+ var threw = false;
+ try
+ {
+ converter.ConvertFrom(null, System.Globalization.CultureInfo.InvariantCulture, invalidValue);
+ }
+ catch (FormatException)
+ {
+ threw = true;
+ }
+
+ Assert.IsTrue(threw, "Converting '0' to DateTime should throw FormatException");
+ }
+
+ ///
+ /// Test various invalid DateTime strings that might be stored in GenericAttributes.
+ ///
+ [TestMethod]
+ [DataRow("0")]
+ [DataRow("null")]
+ [DataRow("undefined")]
+ [DataRow("N/A")]
+ public void CommonHelperSimulation_InvalidDateStrings_ThrowFormatException(string invalidValue)
+ {
+ var converter = System.ComponentModel.TypeDescriptor.GetConverter(typeof(DateTime));
+
+ var threw = false;
+ try
+ {
+ converter.ConvertFrom(null, System.Globalization.CultureInfo.InvariantCulture, invalidValue);
+ }
+ catch (FormatException)
+ {
+ threw = true;
+ }
+
+ Assert.IsTrue(threw, $"Converting '{invalidValue}' to DateTime should throw FormatException");
+ }
+
+ #endregion
+}
diff --git a/AyCode.Core.Tests/TestModels/SharedTestModels.cs b/AyCode.Core.Tests/TestModels/SharedTestModels.cs
index 82e7988..2a60223 100644
--- a/AyCode.Core.Tests/TestModels/SharedTestModels.cs
+++ b/AyCode.Core.Tests/TestModels/SharedTestModels.cs
@@ -284,6 +284,28 @@ public class TestGuidItem : IId
#region Test-specific classes
+///
+/// Simulates NopCommerce GenericAttribute - stores key-value pairs where DateTime values
+/// are stored as strings in the database.
+///
+public class TestGenericAttribute
+{
+ public int Id { get; set; }
+ public string Key { get; set; } = "";
+ public string Value { get; set; } = "";
+}
+
+///
+/// DTO with GenericAttributes collection - simulates OrderDto with string-stored DateTime values.
+/// This reproduces the production bug where Binary serialization was thought to corrupt DateTime strings.
+///
+public class TestDtoWithGenericAttributes : IId
+{
+ public int Id { get; set; }
+ public string Name { get; set; } = "";
+ public List GenericAttributes { get; set; } = [];
+}
+
///
/// Order with nullable collections for null vs empty testing
///
diff --git a/AyCode.Core/Extensions/AcBinaryDeserializer.cs b/AyCode.Core/Extensions/AcBinaryDeserializer.cs
index 68eabb6..29b6908 100644
--- a/AyCode.Core/Extensions/AcBinaryDeserializer.cs
+++ b/AyCode.Core/Extensions/AcBinaryDeserializer.cs
@@ -288,6 +288,9 @@ public static class AcBinaryDeserializer
///
/// Read a string and register it in the intern table for future references.
+ /// The serializer registers strings that meet MinStringInternLength (default: 4 chars),
+ /// then subsequent occurrences use StringInterned references.
+ /// We must register strings in the SAME order as the serializer to maintain index consistency.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static string ReadAndInternString(ref BinaryDeserializationContext context)
@@ -295,8 +298,16 @@ public static class AcBinaryDeserializer
var length = (int)context.ReadVarUInt();
if (length == 0) return string.Empty;
var str = context.ReadString(length);
- // Register for future StringInterned references
- context.RegisterInternedString(str);
+ // Always register strings that meet the minimum intern length threshold
+ // This must match the serializer's behavior exactly.
+ // The serializer checks value.Length (char count), not UTF-8 byte length.
+ // Default MinStringInternLength is 4.
+ // IMPORTANT: We register ALL strings >= 4 chars because the serializer does too,
+ // regardless of whether they will be referenced later via StringInterned.
+ if (str.Length >= context.MinStringInternLength)
+ {
+ context.RegisterInternedString(str);
+ }
return str;
}
@@ -1085,6 +1096,12 @@ public static class AcBinaryDeserializer
public byte FormatVersion { get; private set; }
public bool HasMetadata { get; private set; }
public bool HasReferenceHandling { get; private set; }
+
+ ///
+ /// Minimum string length for interning. Must match serializer's MinStringInternLength.
+ /// Default: 4 (from AcBinarySerializerOptions)
+ ///
+ public byte MinStringInternLength { get; private set; }
// Property name table
private string[]? _propertyNames;
@@ -1106,6 +1123,7 @@ public static class AcBinaryDeserializer
FormatVersion = 0;
HasMetadata = false;
HasReferenceHandling = true; // Assume true by default
+ MinStringInternLength = 4; // Default from AcBinarySerializerOptions
_propertyNames = null;
_internedStrings = null;
_references = null;
diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs
index 47670bf..9d91ddb 100644
--- a/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs
+++ b/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs
@@ -7,16 +7,18 @@ using MessagePack.Resolvers;
namespace AyCode.Services.Server.Tests.SignalRs;
///
-/// Integration tests for SignalR client-to-hub communication.
+/// Base class for SignalR client-to-hub communication tests.
/// Tests the full round-trip: Client -> Server -> Service -> Response -> Client
+/// Derived classes specify the serializer type (JSON or Binary).
///
-[TestClass]
-public class SignalRClientToHubTest
+public abstract class SignalRClientToHubTestBase
{
- private TestLogger _logger = null!;
- private TestableSignalRClient2 _client = null!;
- private TestableSignalRHub2 _hub = null!;
- private TestSignalRService2 _service = null!;
+ protected abstract AcSerializerType SerializerType { get; }
+
+ protected TestLogger _logger = null!;
+ protected TestableSignalRClient2 _client = null!;
+ protected TestableSignalRHub2 _hub = null!;
+ protected TestSignalRService2 _service = null!;
[TestInitialize]
public void Setup()
@@ -25,6 +27,8 @@ public class SignalRClientToHubTest
_hub = new TestableSignalRHub2();
_service = new TestSignalRService2();
_client = new TestableSignalRClient2(_hub, _logger);
+
+ _hub.SetSerializerType(SerializerType);
_hub.RegisterService(_service, _client);
}
@@ -162,7 +166,7 @@ public class SignalRClientToHubTest
Assert.IsNotNull(result);
Assert.AreEqual(1, result.Id);
Assert.AreEqual("Processed: Widget", result.ProductName);
- Assert.AreEqual(item.Quantity * 2, result.Quantity); // Doubled
+ Assert.AreEqual(item.Quantity * 2, result.Quantity);
Assert.AreEqual(item.UnitPrice * 2, result.UnitPrice);
}
@@ -194,7 +198,7 @@ public class SignalRClientToHubTest
#endregion
- #region Collection Parameter Tests (using PostDataAsync for complex types)
+ #region Collection Parameter Tests
[TestMethod]
public async Task Post_IntArray_ReturnsDoubledValues()
@@ -312,25 +316,15 @@ public class SignalRClientToHubTest
CollectionAssert.AreEqual(input, result);
}
- ///
- /// REGRESSION TEST: Tests GetAllAsync with empty array as context parameter.
- /// Bug: When passing an empty array via GetAllAsync with contextParams,
- /// the server receives an object instead of an array, causing deserialization failure.
- /// Error: "JSON is an object but target type 'Int32[]' is a collection type"
- ///
[TestMethod]
public async Task GetAll_WithEmptyArrayContextParam_ReturnsResult()
{
var result = await _client.GetAllAsync(TestSignalRTags.IntArrayParam, [Array.Empty()]);
Assert.IsNotNull(result, "Result should not be null");
- // Empty array doubled is still empty array
Assert.AreEqual(0, result.Length, "Empty array should return empty array");
}
- ///
- /// Tests GetAllAsync with non-empty array as context parameter.
- ///
[TestMethod]
public async Task GetAll_WithArrayContextParam_ReturnsDoubledValues()
{
@@ -504,7 +498,7 @@ public class SignalRClientToHubTest
#endregion
- #region Async Task Method Tests - Critical for detecting non-awaited Tasks
+ #region Async Task Method Tests
[TestMethod]
public async Task Async_TestOrderItem_ReturnsProcessedItem()
@@ -548,12 +542,11 @@ public class SignalRClientToHubTest
#endregion
- #region MessagePack Round-Trip Integrity Tests
+ #region Round-Trip Integrity Tests
[TestMethod]
- public async Task MessagePack_ComplexObject_PreservesAllProperties()
+ public async Task RoundTrip_ComplexObject_PreservesAllProperties()
{
- // Test that complex objects survive the full MessagePack round-trip
var item = new TestOrderItem
{
Id = 999,
@@ -568,14 +561,13 @@ public class SignalRClientToHubTest
Assert.IsNotNull(result);
Assert.AreEqual(999, result.Id);
Assert.AreEqual("Processed: RoundTrip Test Item", result.ProductName);
- Assert.AreEqual(100, result.Quantity); // Doubled by service
- Assert.AreEqual(246.90m, result.UnitPrice); // Doubled by service
+ Assert.AreEqual(100, result.Quantity);
+ Assert.AreEqual(246.90m, result.UnitPrice);
}
[TestMethod]
- public async Task MessagePack_NestedOrder_PreservesHierarchy()
+ public async Task RoundTrip_NestedOrder_PreservesHierarchy()
{
- // Test deeply nested object structure survives round-trip
var order = TestDataFactory.CreateOrder(itemCount: 3, palletsPerItem: 2, measurementsPerPallet: 1);
var result = await _client.PostDataAsync(TestSignalRTags.TestOrderParam, order);
@@ -585,7 +577,6 @@ public class SignalRClientToHubTest
Assert.AreEqual(order.OrderNumber, result.OrderNumber);
Assert.AreEqual(3, result.Items.Count);
- // Verify nested items preserved
for (int i = 0; i < 3; i++)
{
Assert.AreEqual(order.Items[i].Id, result.Items[i].Id);
@@ -594,9 +585,8 @@ public class SignalRClientToHubTest
}
[TestMethod]
- public async Task MessagePack_SpecialCharacters_PreservedCorrectly()
+ public async Task RoundTrip_SpecialCharacters_PreservedCorrectly()
{
- // Test that special characters survive JSON serialization in MessagePack payload
var testString = "Special: \"quotes\" 'apostrophes' & ampersand \\ backslash \n newline";
var result = await _client.PostDataAsync(TestSignalRTags.StringParam, testString);
@@ -606,9 +596,8 @@ public class SignalRClientToHubTest
}
[TestMethod]
- public async Task MessagePack_UnicodeCharacters_PreservedCorrectly()
+ public async Task RoundTrip_UnicodeCharacters_PreservedCorrectly()
{
- // Test that Unicode characters survive the round-trip
var testString = "Unicode: 中文 日本語 한국어 🎉 émoji";
var result = await _client.PostDataAsync(TestSignalRTags.StringParam, testString);
@@ -618,7 +607,7 @@ public class SignalRClientToHubTest
}
[TestMethod]
- public async Task MessagePack_EmptyString_PreservedCorrectly()
+ public async Task RoundTrip_EmptyString_PreservedCorrectly()
{
var result = await _client.PostDataAsync(TestSignalRTags.StringParam, "");
@@ -627,7 +616,7 @@ public class SignalRClientToHubTest
}
[TestMethod]
- public async Task MessagePack_LargeDecimal_PreservedCorrectly()
+ public async Task RoundTrip_LargeDecimal_PreservedCorrectly()
{
var largeDecimal = 999999999999.999999m;
@@ -637,7 +626,7 @@ public class SignalRClientToHubTest
}
[TestMethod]
- public async Task MessagePack_ExtremeInt_PreservedCorrectly()
+ public async Task RoundTrip_ExtremeInt_PreservedCorrectly()
{
var result = await _client.PostDataAsync(TestSignalRTags.SingleIntParam, int.MaxValue);
@@ -646,29 +635,24 @@ public class SignalRClientToHubTest
#endregion
- #region JSON Serialization Integrity Tests
+ #region Response Data Integrity Tests
[TestMethod]
- public async Task Json_ResponseData_NotDoubleEscaped()
+ public async Task ResponseData_NotDoubleEscaped()
{
- // This test ensures the JSON in ResponseData is not double-escaped
- // which was the original bug we fixed
var item = new TestOrderItem { Id = 1, ProductName = "Test", Quantity = 10, UnitPrice = 20m };
var result = await _client.PostDataAsync(TestSignalRTags.TestOrderItemParam, item);
- // If double serialization occurred, result would be null or have wrong values
Assert.IsNotNull(result);
Assert.AreEqual(1, result.Id);
Assert.AreEqual("Processed: Test", result.ProductName);
-
- // Verify numeric values survived correctly (would fail with double-escaped JSON)
Assert.AreEqual(20, result.Quantity);
Assert.AreEqual(40m, result.UnitPrice);
}
[TestMethod]
- public async Task Json_CollectionResponse_DeserializesCorrectly()
+ public async Task CollectionResponse_DeserializesCorrectly()
{
var items = new List
{
@@ -690,16 +674,6 @@ public class SignalRClientToHubTest
}
}
- ///
- /// CRITICAL TEST: Ensures async Task<T> methods are properly awaited before serialization.
- ///
- /// Bug scenario: If an async method returns Task<TestOrderItem> and the framework
- /// serializes the Task object instead of awaiting it, the JSON looks like:
- /// {"Result":{"Id":1,"ProductName":"..."},"Status":5,"IsCompleted":true,"IsCompletedSuccessfully":true}
- ///
- /// The actual data is wrapped in "Result" property, and Task metadata pollutes the response.
- /// This test ensures we get the actual object, not the Task wrapper.
- ///
[TestMethod]
public async Task Async_Method_ReturnsActualResult_NotTaskWrapper()
{
@@ -707,32 +681,18 @@ public class SignalRClientToHubTest
var result = await _client.PostDataAsync(TestSignalRTags.AsyncTestOrderItemParam, item);
- // If Task was serialized instead of awaited, result would have wrong values:
- // - Id would still be correct (Task.Id coincidentally matches)
- // - ProductName would be null/empty (not at root level)
- // - Quantity would be 0 (not at root level)
Assert.IsNotNull(result, "Result should not be null");
Assert.AreEqual(42, result.Id, "Id should match the original");
Assert.AreEqual("Async: TestProduct", result.ProductName,
- "ProductName should be 'Async: TestProduct', not null. " +
- "If null, the async method result was not properly awaited and Task was serialized.");
- Assert.AreEqual(15, result.Quantity,
- "Quantity should be tripled (5*3=15), not 0. " +
- "If 0, the async method result was not properly awaited.");
- Assert.AreEqual(30m, result.UnitPrice,
- "UnitPrice should be tripled (10*3=30), not 0. " +
- "If 0, the async method result was not properly awaited.");
+ "ProductName should be 'Async: TestProduct', not null.");
+ Assert.AreEqual(15, result.Quantity, "Quantity should be tripled (5*3=15).");
+ Assert.AreEqual(30m, result.UnitPrice, "UnitPrice should be tripled (10*3=30).");
}
#endregion
- #region Task.FromResult Integration Tests - CRITICAL for production bug coverage
+ #region Task.FromResult Integration Tests
- ///
- /// CRITICAL TEST: Tests full SignalR round-trip with Task.FromResult<string>.
- /// This was the root cause of the production bug where methods returning Task
- /// without async keyword caused Task wrapper to be serialized.
- ///
[TestMethod]
public async Task TaskFromResult_String_ReturnsActualResult_NotTaskWrapper()
{
@@ -743,14 +703,6 @@ public class SignalRClientToHubTest
"Should return the actual string, not Task wrapper JSON");
}
- ///
- /// CRITICAL TEST: Tests full SignalR round-trip with Task.FromResult<TestOrderItem>.
- /// This simulates the exact production bug scenario where complex objects returned
- /// via Task.FromResult were serialized as Task wrapper instead of the actual object.
- ///
- /// Bug JSON output was:
- /// {"Result":{"Id":1,"ProductName":"..."},"Status":5,"IsCompleted":true,"IsCompletedSuccessfully":true}
- ///
[TestMethod]
public async Task TaskFromResult_ComplexObject_ReturnsActualResult_NotTaskWrapper()
{
@@ -758,47 +710,26 @@ public class SignalRClientToHubTest
var result = await _client.PostDataAsync(TestSignalRTags.TaskFromResultTestOrderItemParam, item);
- // If Task wrapper was serialized, these assertions would fail:
- // - ProductName would be null (nested inside "Result" property)
- // - Quantity would be 0
- // - UnitPrice would be 0
Assert.IsNotNull(result, "Result should not be null");
Assert.AreEqual(42, result.Id, "Id should match the original");
Assert.AreEqual("FromResult: TestProduct", result.ProductName,
- "ProductName should be 'FromResult: TestProduct'. " +
- "If null, Task.FromResult result was not properly unwrapped.");
- Assert.AreEqual(10, result.Quantity,
- "Quantity should be doubled (5*2=10). " +
- "If 0, Task.FromResult result was not properly unwrapped.");
- Assert.AreEqual(20m, result.UnitPrice,
- "UnitPrice should be doubled (10*2=20). " +
- "If 0, Task.FromResult result was not properly unwrapped.");
+ "ProductName should be 'FromResult: TestProduct'.");
+ Assert.AreEqual(10, result.Quantity, "Quantity should be doubled (5*2=10).");
+ Assert.AreEqual(20m, result.UnitPrice, "UnitPrice should be doubled (10*2=20).");
}
- ///
- /// Tests Task.FromResult<int> - primitive value type via Task.FromResult.
- ///
[TestMethod]
public async Task TaskFromResult_Int_ReturnsActualResult_NotTaskWrapper()
{
var result = await _client.PostDataAsync(TestSignalRTags.TaskFromResultIntParam, 42);
- Assert.AreEqual(84, result,
- "Should return doubled value (42*2=84). " +
- "If 0 or wrong value, Task.FromResult was not properly unwrapped.");
+ Assert.AreEqual(84, result, "Should return doubled value (42*2=84).");
}
- ///
- /// Tests Task.CompletedTask (non-generic Task) via SignalR.
- ///
[TestMethod]
public async Task TaskFromResult_NoResult_CompletesSuccessfully()
{
- // This should not throw and should complete successfully
var result = await _client.GetAllAsync