1981 lines
76 KiB
C#
1981 lines
76 KiB
C#
using System.Runtime.Serialization;
|
|
using AyCode.Core.Enums;
|
|
using AyCode.Core.Extensions;
|
|
using AyCode.Core.Interfaces;
|
|
using AyCode.Core.Loggers;
|
|
using Newtonsoft.Json;
|
|
|
|
namespace AyCode.Core.Tests;
|
|
|
|
[TestClass]
|
|
public sealed class JsonExtensionTests
|
|
{
|
|
[TestInitialize]
|
|
public void TestInit()
|
|
{
|
|
TestDataFactory.ResetIdCounter();
|
|
}
|
|
|
|
private static JsonSerializerSettings GetMergeSettings()
|
|
{
|
|
return SerializeObjectExtensions.Options;
|
|
//return new JsonSerializerSettings
|
|
//{
|
|
// ContractResolver = new UnifiedMergeContractResolver(),
|
|
// Context = new StreamingContext(StreamingContextStates.All, new Dictionary<object, object>()),
|
|
// PreserveReferencesHandling = PreserveReferencesHandling.Objects,
|
|
// ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
|
// NullValueHandling = NullValueHandling.Ignore,
|
|
//};
|
|
}
|
|
|
|
#region Deep Hierarchy Tests (5 Levels)
|
|
|
|
[TestMethod]
|
|
public void DeepHierarchy_5Levels_MergePreservesAllReferences()
|
|
{
|
|
// Arrange: Create 5-level deep hierarchy
|
|
TestDataFactory.ResetIdCounter();
|
|
var order = TestDataFactory.CreateDeepOrder(itemCount: 2, palletsPerItem: 2, measurementsPerPallet: 2, pointsPerMeasurement: 3);
|
|
|
|
// Store original references at all levels
|
|
var originalOrder = order;
|
|
var originalItem = order.Items[0];
|
|
var originalPallet = order.Items[0].Pallets[0];
|
|
var originalMeasurement = order.Items[0].Pallets[0].Measurements[0];
|
|
var originalPoint = order.Items[0].Pallets[0].Measurements[0].Points[0];
|
|
|
|
var settings = GetMergeSettings();
|
|
|
|
// Create update JSON that modifies values at all 5 levels
|
|
var updateJson = $@"{{
|
|
""Id"": {order.Id},
|
|
""OrderNumber"": ""ORD-UPDATED"",
|
|
""Items"": [{{
|
|
""Id"": {originalItem.Id},
|
|
""ProductName"": ""Updated-Product"",
|
|
""Pallets"": [{{
|
|
""Id"": {originalPallet.Id},
|
|
""PalletCode"": ""PLT-UPDATED"",
|
|
""Measurements"": [{{
|
|
""Id"": {originalMeasurement.Id},
|
|
""Name"": ""Measurement-UPDATED"",
|
|
""Points"": [{{
|
|
""Id"": {originalPoint.Id},
|
|
""Label"": ""Point-UPDATED"",
|
|
""Value"": 999.99
|
|
}}]
|
|
}}]
|
|
}}]
|
|
}}]
|
|
}}";
|
|
|
|
// Act
|
|
updateJson.JsonTo(order, settings);
|
|
|
|
// Assert: All references preserved
|
|
Assert.AreSame(originalOrder, order, "Level 1: Order reference must be preserved");
|
|
Assert.AreSame(originalItem, order.Items[0], "Level 2: Item reference must be preserved");
|
|
Assert.AreSame(originalPallet, order.Items[0].Pallets[0], "Level 3: Pallet reference must be preserved");
|
|
Assert.AreSame(originalMeasurement, order.Items[0].Pallets[0].Measurements[0], "Level 4: Measurement reference must be preserved");
|
|
Assert.AreSame(originalPoint, order.Items[0].Pallets[0].Measurements[0].Points[0], "Level 5: Point reference must be preserved");
|
|
|
|
// Assert: Values updated
|
|
Assert.AreEqual("ORD-UPDATED", order.OrderNumber);
|
|
Assert.AreEqual("Updated-Product", order.Items[0].ProductName);
|
|
Assert.AreEqual("PLT-UPDATED", order.Items[0].Pallets[0].PalletCode);
|
|
Assert.AreEqual("Measurement-UPDATED", order.Items[0].Pallets[0].Measurements[0].Name);
|
|
Assert.AreEqual("Point-UPDATED", order.Items[0].Pallets[0].Measurements[0].Points[0].Label);
|
|
Assert.AreEqual(999.99, order.Items[0].Pallets[0].Measurements[0].Points[0].Value);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void DeepHierarchy_5Levels_InsertAndKeepLogic()
|
|
{
|
|
// Arrange
|
|
TestDataFactory.ResetIdCounter();
|
|
var order = TestDataFactory.CreateDeepOrder(itemCount: 2, palletsPerItem: 1, measurementsPerPallet: 1, pointsPerMeasurement: 2);
|
|
|
|
var originalItemCount = order.Items.Count;
|
|
var originalItem2 = order.Items[1]; // This should be KEPT
|
|
var existingPointId = order.Items[0].Pallets[0].Measurements[0].Points[0].Id;
|
|
|
|
var settings = GetMergeSettings();
|
|
|
|
// Update only first item, add new point - second item should be KEPT
|
|
var updateJson = $@"{{
|
|
""Id"": {order.Id},
|
|
""Items"": [{{
|
|
""Id"": {order.Items[0].Id},
|
|
""Pallets"": [{{
|
|
""Id"": {order.Items[0].Pallets[0].Id},
|
|
""Measurements"": [{{
|
|
""Id"": {order.Items[0].Pallets[0].Measurements[0].Id},
|
|
""Points"": [
|
|
{{ ""Id"": {existingPointId}, ""Label"": ""Updated-Point"" }},
|
|
{{ ""Id"": 9999, ""Label"": ""NEW-Point"", ""Value"": 123.45 }}
|
|
]
|
|
}}]
|
|
}}]
|
|
}}]
|
|
}}";
|
|
|
|
// Act
|
|
updateJson.JsonTo(order, settings);
|
|
|
|
// Assert: KEEP logic works at all levels
|
|
Assert.AreEqual(originalItemCount, order.Items.Count, "Items count should be preserved (KEEP logic)");
|
|
Assert.AreSame(originalItem2, order.Items[1], "Second item reference should be preserved (KEEP)");
|
|
|
|
// Assert: INSERT logic works at deepest level
|
|
var points = order.Items[0].Pallets[0].Measurements[0].Points;
|
|
Assert.IsTrue(points.Count >= 2, "Should have at least 2 points (existing + new)");
|
|
Assert.IsTrue(points.Any(p => p.Id == 9999 && p.Label == "NEW-Point"), "New point should be inserted");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Semantic Reference Tests (IId types with long-based semantic IDs)
|
|
|
|
[TestMethod]
|
|
public void SemanticReference_SharedAttribute_SerializesWithSemanticId()
|
|
{
|
|
// Arrange: Create order with shared attribute across multiple properties
|
|
TestDataFactory.ResetIdCounter();
|
|
var sharedAttr = TestDataFactory.CreateSharedAttribute();
|
|
var order = TestDataFactory.CreateDeepOrder(
|
|
itemCount: 2,
|
|
palletsPerItem: 1,
|
|
measurementsPerPallet: 1,
|
|
pointsPerMeasurement: 1,
|
|
sharedAttribute: sharedAttr);
|
|
|
|
var settings = GetMergeSettings();
|
|
settings.Formatting = Formatting.Indented;
|
|
|
|
// Act
|
|
var json = order.ToJson(settings);
|
|
|
|
Console.WriteLine("Semantic Reference JSON:");
|
|
Console.WriteLine(json);
|
|
|
|
// Assert: Should contain $id for IId types (now using long-based semantic IDs)
|
|
Assert.IsTrue(json.Contains("\"$id\""), $"Should contain $id for IId types. JSON:\n{json}");
|
|
|
|
// Assert: $ref used for duplicate semantic references
|
|
Assert.IsTrue(json.Contains("\"$ref\""), $"Should contain $ref for shared attribute references. JSON:\n{json}");
|
|
|
|
// Assert: Shared attribute should be referenced multiple times
|
|
var idCount = json.Split("\"$id\"").Length - 1;
|
|
var refCount = json.Split("\"$ref\"").Length - 1;
|
|
Assert.IsTrue(idCount > 0, $"Should have at least one $id. Found: {idCount}");
|
|
Assert.IsTrue(refCount > 0, $"Should have at least one $ref. Found: {refCount}");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void SemanticReference_DeserializeAndMerge_PreservesSharedReferences()
|
|
{
|
|
// Arrange
|
|
TestDataFactory.ResetIdCounter();
|
|
var sharedAttr = TestDataFactory.CreateSharedAttribute();
|
|
sharedAttr.Key = "OriginalKey";
|
|
|
|
var order = new TestOrder
|
|
{
|
|
Id = 1,
|
|
OrderNumber = "ORD-001",
|
|
PrimaryAttribute = sharedAttr,
|
|
SecondaryAttribute = sharedAttr, // Same reference
|
|
Attributes = [sharedAttr]
|
|
};
|
|
|
|
var originalAttrRef = order.PrimaryAttribute;
|
|
|
|
// Update that modifies the shared attribute
|
|
var updateJson = @"{
|
|
""Id"": 1,
|
|
""OrderNumber"": ""ORD-UPDATED"",
|
|
""PrimaryAttribute"": { ""Id"": 1, ""Key"": ""UpdatedKey"", ""Value"": ""UpdatedValue"" }
|
|
}";
|
|
|
|
var settings = GetMergeSettings();
|
|
|
|
// Act
|
|
updateJson.JsonTo(order, settings);
|
|
|
|
// Assert
|
|
Assert.AreEqual("ORD-UPDATED", order.OrderNumber);
|
|
Assert.AreEqual("UpdatedKey", order.PrimaryAttribute?.Key);
|
|
|
|
// SecondaryAttribute should still reference the same object (wasn't in update JSON)
|
|
Assert.AreSame(originalAttrRef, order.SecondaryAttribute, "SecondaryAttribute reference should be preserved");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Newtonsoft Reference Tests (Non-IId types with numeric $id/$ref)
|
|
|
|
[TestMethod]
|
|
public void NewtonsoftReference_SharedNonIdMetadata_SerializesWithNumericId()
|
|
{
|
|
// Arrange: Create order with shared non-IId metadata
|
|
TestDataFactory.ResetIdCounter();
|
|
var sharedMeta = TestDataFactory.CreateNonIdMetadata(withChild: true);
|
|
var order = TestDataFactory.CreateDeepOrder(
|
|
itemCount: 2,
|
|
palletsPerItem: 1,
|
|
measurementsPerPallet: 1,
|
|
pointsPerMeasurement: 1,
|
|
sharedNonIdMetadata: sharedMeta);
|
|
|
|
var settings = GetMergeSettings();
|
|
settings.Formatting = Formatting.Indented;
|
|
|
|
// Act
|
|
var json = order.ToJson(settings);
|
|
|
|
Console.WriteLine("Newtonsoft Reference JSON:");
|
|
Console.WriteLine(json);
|
|
|
|
// Assert: Should contain $ref for duplicates
|
|
Assert.IsTrue(json.Contains("\"$ref\""), $"Should contain $ref for shared references. JSON:\n{json}");
|
|
|
|
// Assert: Should contain $id for objects
|
|
Assert.IsTrue(json.Contains("\"$id\""), "Should contain $id for objects");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void NewtonsoftReference_DeepNestedNonId_HandlesCorrectly()
|
|
{
|
|
// Arrange: Create chain of non-IId metadata
|
|
var rootMeta = new TestNonIdMetadata
|
|
{
|
|
Key = "Root",
|
|
Value = "RootValue",
|
|
Timestamp = DateTime.UtcNow,
|
|
ChildMetadata = new TestNonIdMetadata
|
|
{
|
|
Key = "Child",
|
|
Value = "ChildValue",
|
|
Timestamp = DateTime.UtcNow,
|
|
ChildMetadata = new TestNonIdMetadata
|
|
{
|
|
Key = "GrandChild",
|
|
Value = "GrandChildValue",
|
|
Timestamp = DateTime.UtcNow
|
|
}
|
|
}
|
|
};
|
|
|
|
var order = new TestOrder
|
|
{
|
|
Id = 1,
|
|
OrderNumber = "ORD-001",
|
|
OrderMetadata = rootMeta,
|
|
AuditMetadata = rootMeta // Same reference
|
|
};
|
|
|
|
var settings = GetMergeSettings();
|
|
settings.Formatting = Formatting.Indented;
|
|
|
|
// Act
|
|
var json = order.ToJson(settings);
|
|
|
|
Console.WriteLine("Deep Non-IId JSON:");
|
|
Console.WriteLine(json);
|
|
|
|
// Assert
|
|
Assert.IsTrue(json.Contains("Root"), "Should contain root metadata");
|
|
Assert.IsTrue(json.Contains("Child"), "Should contain child metadata");
|
|
Assert.IsTrue(json.Contains("GrandChild"), "Should contain grandchild metadata");
|
|
Assert.IsTrue(json.Contains("\"$ref\""), "Should contain $ref for duplicate rootMeta reference");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Hybrid Reference Tests (Mixed IId and Non-IId)
|
|
|
|
[TestMethod]
|
|
public void HybridReference_MixedTypes_BothRefSystemsWork()
|
|
{
|
|
// Arrange: Create complex object with both IId and non-IId shared references
|
|
TestDataFactory.ResetIdCounter();
|
|
var sharedAttr = TestDataFactory.CreateSharedAttribute();
|
|
var sharedMeta = TestDataFactory.CreateNonIdMetadata();
|
|
|
|
// Shared attribute also has nested non-IId metadata
|
|
sharedAttr.NestedMetadata = sharedMeta;
|
|
|
|
var order = new TestOrder
|
|
{
|
|
Id = 1,
|
|
OrderNumber = "ORD-001",
|
|
PrimaryAttribute = sharedAttr,
|
|
SecondaryAttribute = sharedAttr, // IId duplicate
|
|
OrderMetadata = sharedMeta,
|
|
AuditMetadata = sharedMeta, // Non-IId duplicate
|
|
Items =
|
|
[
|
|
new TestOrderItem
|
|
{
|
|
Id = 10,
|
|
ProductName = "Product-A",
|
|
Quantity = 5,
|
|
Attribute = sharedAttr, // Same IId reference again
|
|
ItemMetadata = sharedMeta // Same non-IId reference again
|
|
}
|
|
]
|
|
};
|
|
|
|
var settings = GetMergeSettings();
|
|
settings.Formatting = Formatting.Indented;
|
|
|
|
// Act
|
|
var json = order.ToJson(settings);
|
|
|
|
Console.WriteLine("Hybrid Reference JSON:");
|
|
Console.WriteLine(json);
|
|
|
|
// Assert: Should have $id and $ref tokens for reference handling
|
|
Assert.IsTrue(json.Contains("\"$id\""), "Should have $id tokens for objects");
|
|
Assert.IsTrue(json.Contains("\"$ref\""), "Should have $ref tokens for duplicates");
|
|
|
|
// Count $ref occurrences - should have multiple (for both IId and non-IId duplicates)
|
|
var refCount = json.Split("\"$ref\"").Length - 1;
|
|
Assert.IsTrue(refCount >= 2, $"Should have at least 2 $ref tokens. Found: {refCount}. JSON:\n{json}");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void HybridReference_MergeWithMixedReferences()
|
|
{
|
|
// Arrange
|
|
TestDataFactory.ResetIdCounter();
|
|
var sharedAttr = TestDataFactory.CreateSharedAttribute();
|
|
sharedAttr.Key = "OriginalAttr";
|
|
|
|
var sharedMeta = TestDataFactory.CreateNonIdMetadata();
|
|
sharedMeta.Key = "OriginalMeta";
|
|
|
|
var order = new TestOrder
|
|
{
|
|
Id = 1,
|
|
OrderNumber = "ORD-001",
|
|
PrimaryAttribute = sharedAttr,
|
|
SecondaryAttribute = sharedAttr,
|
|
OrderMetadata = sharedMeta,
|
|
AuditMetadata = sharedMeta,
|
|
Items =
|
|
[
|
|
new TestOrderItem { Id = 10, ProductName = "Product-A", Quantity = 5 },
|
|
new TestOrderItem { Id = 20, ProductName = "Product-B", Quantity = 3 }
|
|
]
|
|
};
|
|
|
|
var originalItem10Ref = order.Items[0];
|
|
var originalItem20Ref = order.Items[1];
|
|
var originalAttrRef = order.PrimaryAttribute;
|
|
var originalMetaRef = order.OrderMetadata;
|
|
|
|
// Update: modify item 10, add item 30, update PrimaryAttribute
|
|
var updateJson = $@"{{
|
|
""Id"": 1,
|
|
""OrderNumber"": ""ORD-UPDATED"",
|
|
""Items"": [
|
|
{{ ""Id"": 10, ""ProductName"": ""Product-A-UPDATED"", ""Quantity"": 50 }},
|
|
{{ ""Id"": 30, ""ProductName"": ""Product-C-NEW"", ""Quantity"": 7 }}
|
|
],
|
|
""PrimaryAttribute"": {{ ""Id"": {sharedAttr.Id}, ""Key"": ""UpdatedAttr"" }}
|
|
}}";
|
|
|
|
var settings = GetMergeSettings();
|
|
|
|
// Act
|
|
updateJson.JsonTo(order, settings);
|
|
|
|
// Assert: References preserved
|
|
Assert.AreSame(originalItem10Ref, order.Items.Single(i => i.Id == 10), "Item 10 reference preserved");
|
|
Assert.AreSame(originalItem20Ref, order.Items.Single(i => i.Id == 20), "Item 20 reference preserved (KEEP)");
|
|
Assert.IsNotNull(order.Items.SingleOrDefault(i => i.Id == 30), "Item 30 inserted");
|
|
|
|
// Values updated
|
|
Assert.AreEqual("ORD-UPDATED", order.OrderNumber);
|
|
Assert.AreEqual("Product-A-UPDATED", order.Items.Single(i => i.Id == 10).ProductName);
|
|
Assert.AreEqual(50, order.Items.Single(i => i.Id == 10).Quantity);
|
|
Assert.AreEqual("UpdatedAttr", order.PrimaryAttribute?.Key);
|
|
|
|
// SecondaryAttribute still references original (wasn't updated)
|
|
Assert.AreSame(originalAttrRef, order.SecondaryAttribute);
|
|
|
|
// Metadata references unchanged
|
|
Assert.AreSame(originalMetaRef, order.OrderMetadata);
|
|
Assert.AreSame(originalMetaRef, order.AuditMetadata);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region NoMerge Collection Tests
|
|
|
|
[TestMethod]
|
|
public void NoMergeCollection_DeepHierarchy_ReplacesEntireCollection()
|
|
{
|
|
// Arrange
|
|
TestDataFactory.ResetIdCounter();
|
|
var order = TestDataFactory.CreateDeepOrder(itemCount: 2);
|
|
|
|
// Add items to NoMergeItems
|
|
order.NoMergeItems =
|
|
[
|
|
new TestOrderItem { Id = 100, ProductName = "NoMerge-A", Quantity = 1 },
|
|
new TestOrderItem { Id = 101, ProductName = "NoMerge-B", Quantity = 2 }
|
|
];
|
|
|
|
var originalNoMergeRef = order.NoMergeItems;
|
|
|
|
var updateJson = $@"{{
|
|
""Id"": {order.Id},
|
|
""NoMergeItems"": [
|
|
{{ ""Id"": 200, ""ProductName"": ""NoMerge-NEW-A"", ""Quantity"": 10 }},
|
|
{{ ""Id"": 201, ""ProductName"": ""NoMerge-NEW-B"", ""Quantity"": 20 }}
|
|
]
|
|
}}";
|
|
|
|
var settings = GetMergeSettings();
|
|
|
|
// Act
|
|
order.DeepPopulateWithMerge(updateJson, settings);
|
|
|
|
// Assert: Collection replaced, not merged
|
|
Assert.AreNotSame(originalNoMergeRef, order.NoMergeItems, "NoMergeItems collection should be replaced");
|
|
Assert.AreEqual(2, order.NoMergeItems.Count);
|
|
Assert.IsFalse(order.NoMergeItems.Any(i => i.Id == 100), "Original item 100 should be gone");
|
|
Assert.IsFalse(order.NoMergeItems.Any(i => i.Id == 101), "Original item 101 should be gone");
|
|
Assert.IsTrue(order.NoMergeItems.Any(i => i.Id == 200), "New item 200 should exist");
|
|
Assert.IsTrue(order.NoMergeItems.Any(i => i.Id == 201), "New item 201 should exist");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Non-IId Collection Tests
|
|
|
|
[TestMethod]
|
|
public void NonIdCollection_ReplacesContent()
|
|
{
|
|
// Arrange
|
|
var order = new TestOrder
|
|
{
|
|
Id = 1,
|
|
OrderNumber = "ORD-001",
|
|
MetadataList =
|
|
[
|
|
new TestNonIdMetadata { Key = "Old-A", Value = "Old-Value-A" },
|
|
new TestNonIdMetadata { Key = "Old-B", Value = "Old-Value-B" }
|
|
]
|
|
};
|
|
|
|
var updateJson = @"{
|
|
""Id"": 1,
|
|
""MetadataList"": [
|
|
{ ""Key"": ""New-X"", ""Value"": ""New-Value-X"" },
|
|
{ ""Key"": ""New-Y"", ""Value"": ""New-Value-Y"" },
|
|
{ ""Key"": ""New-Z"", ""Value"": ""New-Value-Z"" }
|
|
]
|
|
}";
|
|
|
|
var settings = GetMergeSettings();
|
|
|
|
// Act
|
|
order.DeepPopulateWithMerge(updateJson, settings);
|
|
|
|
// Assert: Collection content replaced
|
|
Assert.AreEqual(3, order.MetadataList.Count);
|
|
Assert.IsFalse(order.MetadataList.Any(m => m.Key == "Old-A"));
|
|
Assert.IsTrue(order.MetadataList.Any(m => m.Key == "New-X"));
|
|
Assert.IsTrue(order.MetadataList.Any(m => m.Key == "New-Y"));
|
|
Assert.IsTrue(order.MetadataList.Any(m => m.Key == "New-Z"));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Guid IId Tests
|
|
|
|
[TestMethod]
|
|
public void GuidId_DeepPopulate_ReferencePreserved()
|
|
{
|
|
var order = new TestGuidOrder
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Code = "ORD-001",
|
|
Items =
|
|
[
|
|
new TestGuidItem { Id = Guid.NewGuid(), Name = "Apple", Qty = 5 },
|
|
new TestGuidItem { Id = Guid.NewGuid(), Name = "Orange", Qty = 3 }
|
|
],
|
|
Count = 2
|
|
};
|
|
|
|
var originalItemsRef = order.Items;
|
|
var originalAppleRef = order.Items[0];
|
|
|
|
var json = new
|
|
{
|
|
Id = order.Id,
|
|
Code = "ORD-UPDATED",
|
|
Items = new[]
|
|
{
|
|
new { Id = order.Items[0].Id, Name = "Apple", Qty = 7 },
|
|
new { Id = Guid.NewGuid(), Name = "Banana", Qty = 4 }
|
|
},
|
|
Count = 999
|
|
}.ToJson(GetMergeSettings());
|
|
|
|
json.JsonTo(order, GetMergeSettings());
|
|
|
|
Assert.AreSame(originalItemsRef, order.Items, "List reference must be preserved.");
|
|
Assert.AreSame(originalAppleRef, order.Items[0], "Apple item reference must be preserved.");
|
|
Assert.AreEqual(3, order.Items.Count, "Items should include: updated Apple, new Banana, and kept Orange.");
|
|
Assert.AreEqual(7, order.Items[0].Qty);
|
|
Assert.AreEqual("ORD-UPDATED", order.Code);
|
|
Assert.AreEqual(999, order.Count);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Round-Trip Serialization Tests
|
|
|
|
[TestMethod]
|
|
public void RoundTrip_DeepHierarchy_SerializeAndVerifyStructure()
|
|
{
|
|
// Arrange
|
|
TestDataFactory.ResetIdCounter();
|
|
var sharedAttr = TestDataFactory.CreateSharedAttribute();
|
|
var sharedMeta = TestDataFactory.CreateNonIdMetadata(withChild: true);
|
|
|
|
var originalOrder = TestDataFactory.CreateDeepOrder(
|
|
itemCount: 2,
|
|
palletsPerItem: 2,
|
|
measurementsPerPallet: 2,
|
|
pointsPerMeasurement: 3,
|
|
sharedAttribute: sharedAttr,
|
|
sharedNonIdMetadata: sharedMeta);
|
|
|
|
var settings = GetMergeSettings();
|
|
settings.Formatting = Formatting.Indented;
|
|
|
|
// Act: Serialize
|
|
var json = originalOrder.ToJson(settings);
|
|
|
|
Console.WriteLine("Round-Trip JSON:");
|
|
Console.WriteLine(json);
|
|
|
|
// Assert: JSON structure - should have $id for objects (now using long-based IDs)
|
|
Assert.IsTrue(json.Contains("\"$id\""), "Should have $id for objects");
|
|
|
|
// Assert: JSON structure - $ref for shared references
|
|
Assert.IsTrue(json.Contains("\"$ref\""), "Should have $ref for shared references");
|
|
|
|
// Assert: Data integrity
|
|
Assert.IsTrue(json.Contains(originalOrder.OrderNumber), "Should contain order number");
|
|
Assert.IsTrue(json.Contains(originalOrder.Items[0].ProductName), "Should contain product name");
|
|
|
|
// Assert: Verify the JSON can be deserialized back
|
|
var deserializedOrder = json.JsonTo<TestOrder>(settings);
|
|
Assert.IsNotNull(deserializedOrder);
|
|
Assert.AreEqual(originalOrder.Id, deserializedOrder.Id);
|
|
Assert.AreEqual(originalOrder.OrderNumber, deserializedOrder.OrderNumber);
|
|
Assert.AreEqual(originalOrder.Items.Count, deserializedOrder.Items.Count);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void RoundTrip_MergeIntoExistingObject_PreservesReferences()
|
|
{
|
|
// Arrange: Create original order
|
|
TestDataFactory.ResetIdCounter();
|
|
var originalOrder = TestDataFactory.CreateDeepOrder(itemCount: 2, palletsPerItem: 1, measurementsPerPallet: 1, pointsPerMeasurement: 2);
|
|
|
|
// Store original values
|
|
var originalProductName = originalOrder.Items[0].ProductName;
|
|
|
|
// Store references
|
|
var originalItemRef = originalOrder.Items[0];
|
|
var originalPalletRef = originalOrder.Items[0].Pallets[0];
|
|
|
|
// Create update JSON (without shared non-IId to avoid numeric $ref conflicts)
|
|
var updateJson = $@"{{
|
|
""Id"": {originalOrder.Id},
|
|
""OrderNumber"": ""UPDATED-ORDER"",
|
|
""Items"": [{{
|
|
""Id"": {originalOrder.Items[0].Id},
|
|
""Pallets"": [{{
|
|
""Id"": {originalOrder.Items[0].Pallets[0].Id},
|
|
""PalletCode"": ""UPDATED-PALLET""
|
|
}}]
|
|
}}]
|
|
}}";
|
|
|
|
var settings = GetMergeSettings();
|
|
|
|
// Act
|
|
updateJson.JsonTo(originalOrder, settings);
|
|
|
|
// Assert: References preserved
|
|
Assert.AreSame(originalItemRef, originalOrder.Items[0], "Item reference should be preserved");
|
|
Assert.AreSame(originalPalletRef, originalOrder.Items[0].Pallets[0], "Pallet reference should be preserved");
|
|
|
|
// Assert: Values updated (only what was in the JSON)
|
|
Assert.AreEqual("UPDATED-ORDER", originalOrder.OrderNumber);
|
|
Assert.AreEqual(originalProductName, originalOrder.Items[0].ProductName, "ProductName should be unchanged (not in update JSON)");
|
|
Assert.AreEqual("UPDATED-PALLET", originalOrder.Items[0].Pallets[0].PalletCode);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Test DTO Classes - 5 Levels Deep with Reference Loops
|
|
|
|
/// <summary>
|
|
/// Status enum used across tests
|
|
/// </summary>
|
|
public enum TestStatus
|
|
{
|
|
Pending = 10,
|
|
Processing = 20,
|
|
Shipped = 30
|
|
}
|
|
|
|
/// <summary>
|
|
/// Level 5: Deepest level - Measurement point with value
|
|
/// </summary>
|
|
public class TestMeasurementPoint : IId<int>
|
|
{
|
|
public int Id { get; set; }
|
|
public string Label { get; set; } = string.Empty;
|
|
public double Value { get; set; }
|
|
public DateTime MeasuredAt { get; set; }
|
|
|
|
// Reference loop back to Level 4 (parent)
|
|
[JsonIgnore] // Prevent infinite serialization loop
|
|
public TestMeasurement? ParentMeasurement { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Level 4: Measurement with multiple points
|
|
/// </summary>
|
|
public class TestMeasurement : IId<int>
|
|
{
|
|
public int Id { get; set; }
|
|
public string Name { get; set; } = string.Empty;
|
|
public double TotalWeight { get; set; }
|
|
public DateTime CreatedAt { get; set; }
|
|
|
|
// IId collection - Level 5
|
|
public List<TestMeasurementPoint> Points { get; set; } = [];
|
|
|
|
// Reference loop back to Level 3 (parent)
|
|
[JsonIgnore] public TestPallet? ParentPallet { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Level 3: Pallet containing measurements
|
|
/// </summary>
|
|
public class TestPallet : IId<int>
|
|
{
|
|
public int Id { get; set; }
|
|
public string PalletCode { get; set; } = string.Empty;
|
|
public int TrayCount { get; set; }
|
|
public TestStatus Status { get; set; } = TestStatus.Pending;
|
|
|
|
// IId collection - Level 4
|
|
public List<TestMeasurement> Measurements { get; set; } = [];
|
|
|
|
// Non-IId metadata (for Newtonsoft $ref testing)
|
|
public TestNonIdMetadata? PalletMetadata { get; set; }
|
|
|
|
// Reference loop back to Level 2 (parent)
|
|
[JsonIgnore] public TestOrderItem? ParentOrderItem { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Level 2: Order item with pallets
|
|
/// </summary>
|
|
public class TestOrderItem : IId<int>
|
|
{
|
|
public int Id { get; set; }
|
|
public string ProductName { get; set; } = string.Empty;
|
|
public int Quantity { get; set; }
|
|
public decimal UnitPrice { get; set; }
|
|
public TestStatus ItemStatus { get; set; } = TestStatus.Pending;
|
|
|
|
// IId collection - Level 3
|
|
public List<TestPallet> Pallets { get; set; } = [];
|
|
|
|
// IId object property - shared metadata with semantic $id
|
|
public TestSharedAttribute? Attribute { get; set; }
|
|
|
|
// Non-IId metadata (for Newtonsoft $ref testing)
|
|
public TestNonIdMetadata? ItemMetadata { get; set; }
|
|
|
|
// Reference loop back to Level 1 (parent)
|
|
[JsonIgnore] public TestOrder? ParentOrder { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Level 1: Main order containing items
|
|
/// </summary>
|
|
public class TestOrder : IId<int>
|
|
{
|
|
public int Id { get; set; }
|
|
public string OrderNumber { get; set; } = string.Empty;
|
|
public TestStatus OrderStatus { get; set; } = TestStatus.Pending;
|
|
public DateTime? PaidDateUtc { get; set; }
|
|
public DateTime CreatedAt { get; set; }
|
|
|
|
// IId collection - Level 2
|
|
public List<TestOrderItem> Items { get; set; } = [];
|
|
|
|
// IId collection for attributes (shared references testing)
|
|
public List<TestSharedAttribute> Attributes { get; set; } = [];
|
|
|
|
// Non-IId collection - will be replaced, not merged
|
|
public List<TestNonIdMetadata> MetadataList { get; set; } = [];
|
|
|
|
// Collection with NoMerge attribute - will be replaced
|
|
[JsonNoMergeCollection] public List<TestOrderItem> NoMergeItems { get; set; } = [];
|
|
|
|
// IId object properties for shared reference testing
|
|
public TestSharedAttribute? PrimaryAttribute { get; set; }
|
|
public TestSharedAttribute? SecondaryAttribute { get; set; }
|
|
|
|
// Non-IId object properties for Newtonsoft $ref testing
|
|
public TestNonIdMetadata? OrderMetadata { get; set; }
|
|
public TestNonIdMetadata? AuditMetadata { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shared IId attribute - for semantic $id/$ref testing across objects
|
|
/// </summary>
|
|
public class TestSharedAttribute : IId<int>
|
|
{
|
|
public int Id { get; set; }
|
|
public string Key { get; set; } = string.Empty;
|
|
public string Value { get; set; } = string.Empty;
|
|
public DateTime CreatedOrUpdatedDateUTC { get; set; }
|
|
|
|
// Nested non-IId for mixed reference testing
|
|
public TestNonIdMetadata? NestedMetadata { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Non-IId metadata class - uses Newtonsoft PreserveReferencesHandling (numeric $id/$ref)
|
|
/// </summary>
|
|
public class TestNonIdMetadata
|
|
{
|
|
public string Key { get; set; } = string.Empty;
|
|
public string Value { get; set; } = string.Empty;
|
|
public DateTime Timestamp { get; set; }
|
|
|
|
// Nested non-IId for deep Newtonsoft reference testing
|
|
public TestNonIdMetadata? ChildMetadata { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Order DTO with Guid Id - for testing Guid-based IId
|
|
/// </summary>
|
|
public class TestGuidOrder : IId<Guid>
|
|
{
|
|
public Guid Id { get; set; }
|
|
public string Code { get; set; } = string.Empty;
|
|
public List<TestGuidItem> Items { get; set; } = [];
|
|
public int Count { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Item DTO with Guid Id - for testing Guid-based IId
|
|
/// </summary>
|
|
public class TestGuidItem : IId<Guid>
|
|
{
|
|
public Guid Id { get; set; }
|
|
public string Name { get; set; } = string.Empty;
|
|
public int Qty { get; set; }
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Test Data Factory
|
|
|
|
/// <summary>
|
|
/// Factory for creating deep test object hierarchies
|
|
/// </summary>
|
|
public static class TestDataFactory
|
|
{
|
|
private static int _idCounter = 1;
|
|
|
|
public static void ResetIdCounter() => _idCounter = 1;
|
|
|
|
public static TestOrder CreateDeepOrder(
|
|
int itemCount = 2,
|
|
int palletsPerItem = 2,
|
|
int measurementsPerPallet = 2,
|
|
int pointsPerMeasurement = 3,
|
|
TestNonIdMetadata? sharedNonIdMetadata = null,
|
|
TestSharedAttribute? sharedAttribute = null)
|
|
{
|
|
var order = new TestOrder
|
|
{
|
|
Id = _idCounter++,
|
|
OrderNumber = $"ORD-{_idCounter:D4}",
|
|
OrderStatus = TestStatus.Pending,
|
|
CreatedAt = DateTime.UtcNow,
|
|
PrimaryAttribute = sharedAttribute,
|
|
SecondaryAttribute = sharedAttribute, // Same reference for $ref testing
|
|
OrderMetadata = sharedNonIdMetadata,
|
|
AuditMetadata = sharedNonIdMetadata // Same reference for Newtonsoft $ref
|
|
};
|
|
|
|
if (sharedAttribute != null)
|
|
{
|
|
order.Attributes.Add(sharedAttribute);
|
|
}
|
|
|
|
for (int i = 0; i < itemCount; i++)
|
|
{
|
|
var item = CreateOrderItem(palletsPerItem, measurementsPerPallet, pointsPerMeasurement, sharedNonIdMetadata, sharedAttribute);
|
|
item.ParentOrder = order; // Reference loop
|
|
order.Items.Add(item);
|
|
}
|
|
|
|
return order;
|
|
}
|
|
|
|
public static TestOrderItem CreateOrderItem(
|
|
int palletCount = 2,
|
|
int measurementsPerPallet = 2,
|
|
int pointsPerMeasurement = 3,
|
|
TestNonIdMetadata? sharedNonIdMetadata = null,
|
|
TestSharedAttribute? sharedAttribute = null)
|
|
{
|
|
var item = new TestOrderItem
|
|
{
|
|
Id = _idCounter++,
|
|
ProductName = $"Product-{_idCounter}",
|
|
Quantity = 10 + _idCounter,
|
|
UnitPrice = 5.5m * _idCounter,
|
|
ItemStatus = TestStatus.Pending,
|
|
Attribute = sharedAttribute,
|
|
ItemMetadata = sharedNonIdMetadata
|
|
};
|
|
|
|
for (int i = 0; i < palletCount; i++)
|
|
{
|
|
var pallet = CreatePallet(measurementsPerPallet, pointsPerMeasurement, sharedNonIdMetadata);
|
|
pallet.ParentOrderItem = item; // Reference loop
|
|
item.Pallets.Add(pallet);
|
|
}
|
|
|
|
return item;
|
|
}
|
|
|
|
public static TestPallet CreatePallet(
|
|
int measurementCount = 2,
|
|
int pointsPerMeasurement = 3,
|
|
TestNonIdMetadata? sharedNonIdMetadata = null)
|
|
{
|
|
var pallet = new TestPallet
|
|
{
|
|
Id = _idCounter++,
|
|
PalletCode = $"PLT-{_idCounter:D4}",
|
|
TrayCount = 5 + _idCounter % 10,
|
|
Status = TestStatus.Pending,
|
|
PalletMetadata = sharedNonIdMetadata
|
|
};
|
|
|
|
for (int i = 0; i < measurementCount; i++)
|
|
{
|
|
var measurement = CreateMeasurement(pointsPerMeasurement);
|
|
measurement.ParentPallet = pallet; // Reference loop
|
|
pallet.Measurements.Add(measurement);
|
|
}
|
|
|
|
return pallet;
|
|
}
|
|
|
|
public static TestMeasurement CreateMeasurement(int pointCount = 3)
|
|
{
|
|
var measurement = new TestMeasurement
|
|
{
|
|
Id = _idCounter++,
|
|
Name = $"Measurement-{_idCounter}",
|
|
TotalWeight = 100.5 + _idCounter,
|
|
CreatedAt = DateTime.UtcNow
|
|
};
|
|
|
|
for (int i = 0; i < pointCount; i++)
|
|
{
|
|
var point = CreateMeasurementPoint();
|
|
point.ParentMeasurement = measurement; // Reference loop
|
|
measurement.Points.Add(point);
|
|
}
|
|
|
|
return measurement;
|
|
}
|
|
|
|
public static TestMeasurementPoint CreateMeasurementPoint()
|
|
{
|
|
return new TestMeasurementPoint
|
|
{
|
|
Id = _idCounter++,
|
|
Label = $"Point-{_idCounter}",
|
|
Value = 10.5 + (_idCounter * 0.1),
|
|
MeasuredAt = DateTime.UtcNow
|
|
};
|
|
}
|
|
|
|
public static TestSharedAttribute CreateSharedAttribute()
|
|
{
|
|
return new TestSharedAttribute
|
|
{
|
|
Id = _idCounter++,
|
|
Key = $"Attr-{_idCounter}",
|
|
Value = $"Value-{_idCounter}",
|
|
CreatedOrUpdatedDateUTC = DateTime.UtcNow
|
|
};
|
|
}
|
|
|
|
public static TestNonIdMetadata CreateNonIdMetadata(bool withChild = false)
|
|
{
|
|
return new TestNonIdMetadata
|
|
{
|
|
Key = $"Meta-{_idCounter++}",
|
|
Value = $"MetaValue-{_idCounter}",
|
|
Timestamp = DateTime.UtcNow,
|
|
ChildMetadata = withChild ? CreateNonIdMetadata(false) : null
|
|
};
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Primitive Element Array Tests (SignalR IdMessage pattern fix)
|
|
|
|
/// <summary>
|
|
/// Test that reproduces the exact SignalR loadRelations=true becoming false issue.
|
|
/// IdMessage wraps primitives in arrays: (new[] { x }).ToJson() -> "[true]" string
|
|
/// Server deserializes: "[true]".JsonTo(typeof(bool[])) -> should return bool[] with true
|
|
/// </summary>
|
|
[TestMethod]
|
|
public void PrimitiveArray_BooleanTrue_SerializesAndDeserializesCorrectly()
|
|
{
|
|
// Arrange - This is exactly what IdMessage constructor does
|
|
var loadRelations = true;
|
|
var settings = GetMergeSettings();
|
|
|
|
// Step 1: Client side - IdMessage constructor wraps in array and serializes
|
|
var jsonString = (new[] { loadRelations }).ToJson(settings);
|
|
Console.WriteLine($"Step 1 - Client serialized [true]: {jsonString}");
|
|
|
|
// Step 2: Server side - ProcessOnReceiveMessage deserializes
|
|
// This is: paramValues[i] = (obj.JsonTo(a.GetType()) as Array)?.GetValue(0)!;
|
|
var targetArrayType = typeof(bool).MakeArrayType();
|
|
var deserializedArray = jsonString.JsonTo(targetArrayType, settings) as Array;
|
|
var deserializedValue = deserializedArray?.GetValue(0);
|
|
|
|
Console.WriteLine($"Step 2 - Server deserialized value: {deserializedValue} (type: {deserializedValue?.GetType()})");
|
|
|
|
// Assert
|
|
Assert.IsNotNull(deserializedArray, "Deserialized array should not be null");
|
|
Assert.AreEqual(1, deserializedArray.Length, "Array should have 1 element");
|
|
Assert.IsInstanceOfType(deserializedValue, typeof(bool), "Value should be bool");
|
|
Assert.IsTrue((bool)deserializedValue!, "Boolean true should deserialize as true, NOT false!");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void PrimitiveArray_BooleanFalse_SerializesAndDeserializesCorrectly()
|
|
{
|
|
var loadRelations = false;
|
|
var settings = GetMergeSettings();
|
|
|
|
var jsonString = (new[] { loadRelations }).ToJson(settings);
|
|
Console.WriteLine($"Serialized [false]: {jsonString}");
|
|
|
|
var targetArrayType = typeof(bool).MakeArrayType();
|
|
var deserializedArray = jsonString.JsonTo(targetArrayType, settings) as Array;
|
|
var deserializedValue = deserializedArray?.GetValue(0);
|
|
|
|
Console.WriteLine($"Deserialized value: {deserializedValue}");
|
|
|
|
Assert.IsNotNull(deserializedArray);
|
|
Assert.IsFalse((bool)deserializedValue!, "Boolean false should deserialize as false");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void PrimitiveArray_Int_SerializesAndDeserializesCorrectly()
|
|
{
|
|
var pageSize = 42;
|
|
var settings = GetMergeSettings();
|
|
|
|
var jsonString = (new[] { pageSize }).ToJson(settings);
|
|
Console.WriteLine($"Serialized [42]: {jsonString}");
|
|
|
|
var targetArrayType = typeof(int).MakeArrayType();
|
|
var deserializedArray = jsonString.JsonTo(targetArrayType, settings) as Array;
|
|
var deserializedValue = deserializedArray?.GetValue(0);
|
|
|
|
Console.WriteLine($"Deserialized value: {deserializedValue}");
|
|
|
|
Assert.IsNotNull(deserializedArray);
|
|
Assert.AreEqual(42, (int)deserializedValue!, "Int 42 should deserialize as 42");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void PrimitiveArray_Guid_SerializesAndDeserializesCorrectly()
|
|
{
|
|
var id = Guid.NewGuid();
|
|
var settings = GetMergeSettings();
|
|
|
|
var jsonString = (new[] { id }).ToJson(settings);
|
|
Console.WriteLine($"Serialized [{id}]: {jsonString}");
|
|
|
|
var targetArrayType = typeof(Guid).MakeArrayType();
|
|
var deserializedArray = jsonString.JsonTo(targetArrayType, settings) as Array;
|
|
var deserializedValue = deserializedArray?.GetValue(0);
|
|
|
|
Console.WriteLine($"Deserialized value: {deserializedValue}");
|
|
|
|
Assert.IsNotNull(deserializedArray);
|
|
Assert.AreEqual(id, (Guid)deserializedValue!, "Guid should deserialize correctly");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void PrimitiveArray_String_SerializesAndDeserializesCorrectly()
|
|
{
|
|
// Arrange - String with special characters that need escaping
|
|
var item = new TestOrderItem
|
|
{
|
|
Id = 1,
|
|
ProductName = "Test \"quoted\" \\ backslash \n newline \t tab \r return"
|
|
};
|
|
|
|
// Act
|
|
var json = AcJsonSerializer.Serialize(item);
|
|
|
|
// Assert - JSON should have escaped characters
|
|
Assert.IsTrue(json.Contains("\\\""), "Quotes should be escaped");
|
|
Assert.IsTrue(json.Contains("\\\\"), "Backslashes should be escaped");
|
|
Assert.IsTrue(json.Contains("\\n"), "Newlines should be escaped");
|
|
Assert.IsTrue(json.Contains("\\t"), "Tabs should be escaped");
|
|
|
|
// Round-trip
|
|
var deserialized = AcJsonDeserializer.Deserialize<TestOrderItem>(json);
|
|
Assert.AreEqual("Test \"quoted\" \\ backslash \n newline \t tab \r return",
|
|
deserialized?.ProductName);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void PrimitiveArray_Enum_SerializesAndDeserializesCorrectly()
|
|
{
|
|
var status = TestStatus.Processing;
|
|
var settings = GetMergeSettings();
|
|
|
|
var jsonString = (new[] { status }).ToJson(settings);
|
|
Console.WriteLine($"Serialized [TestStatus.Processing]: {jsonString}");
|
|
|
|
var targetArrayType = typeof(TestStatus).MakeArrayType();
|
|
var deserializedArray = jsonString.JsonTo(targetArrayType, settings) as Array;
|
|
var deserializedValue = deserializedArray?.GetValue(0);
|
|
|
|
Console.WriteLine($"Deserialized value: {deserializedValue}");
|
|
|
|
Assert.IsNotNull(deserializedArray);
|
|
Assert.AreEqual(TestStatus.Processing, (TestStatus)deserializedValue!, "Enum should deserialize correctly");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void PrimitiveArray_DateTime_SerializesAndDeserializesCorrectly()
|
|
{
|
|
var dateTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc);
|
|
var settings = GetMergeSettings();
|
|
|
|
var jsonString = (new[] { dateTime }).ToJson(settings);
|
|
Console.WriteLine($"Serialized [{dateTime}]: {jsonString}");
|
|
|
|
var targetArrayType = typeof(DateTime).MakeArrayType();
|
|
var deserializedArray = jsonString.JsonTo(targetArrayType, settings) as Array;
|
|
var deserializedValue = deserializedArray?.GetValue(0);
|
|
|
|
Console.WriteLine($"Deserialized value: {deserializedValue}");
|
|
|
|
Assert.IsNotNull(deserializedArray);
|
|
Assert.AreEqual(dateTime, (DateTime)deserializedValue!, "DateTime should deserialize correctly");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void PrimitiveArray_Decimal_SerializesAndDeserializesCorrectly()
|
|
{
|
|
var price = 123.45m;
|
|
var settings = GetMergeSettings();
|
|
|
|
var jsonString = (new[] { price }).ToJson(settings);
|
|
Console.WriteLine($"Serialized [{price}]: {jsonString}");
|
|
|
|
var targetArrayType = typeof(decimal).MakeArrayType();
|
|
var deserializedArray = jsonString.JsonTo(targetArrayType, settings) as Array;
|
|
var deserializedValue = deserializedArray?.GetValue(0);
|
|
|
|
Console.WriteLine($"Deserialized value: {deserializedValue}");
|
|
|
|
Assert.IsNotNull(deserializedArray);
|
|
Assert.AreEqual(price, (decimal)deserializedValue!, "Decimal should deserialize correctly");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void PrimitiveArray_MultipleParameters_SimulateSignalRCall()
|
|
{
|
|
// Simulate: GetStockTakings(loadRelations: true, filter: "test", pageSize: 100)
|
|
// Client sends: contextParams = new object[] { true, "test", 100 }
|
|
// IdMessage wraps each param: Ids.Add((new[] { param }).ToJson()) -> "[true]", "[\"test\"]", "[100]"
|
|
var settings = GetMergeSettings();
|
|
|
|
// Simulate IdMessage constructor
|
|
var contextParams = new object[] { true, "filterText", 100, Guid.NewGuid(), TestStatus.Processing };
|
|
var serializedParams = new List<string>();
|
|
foreach (var param in contextParams)
|
|
{
|
|
// This is what IdMessage does: item = (new[] { x }).ToJson();
|
|
var wrapped = Array.CreateInstance(param.GetType(), 1);
|
|
wrapped.SetValue(param, 0);
|
|
var json = wrapped.ToJson(settings);
|
|
serializedParams.Add(json);
|
|
Console.WriteLine($"IdMessage serialized: {param.GetType().Name} '{param}' -> {json}");
|
|
}
|
|
|
|
// Simulate ProcessOnReceiveMessage deserialization
|
|
var paramTypes = new[] { typeof(bool), typeof(string), typeof(int), typeof(Guid), typeof(TestStatus) };
|
|
var deserializedParams = new object[paramTypes.Length];
|
|
|
|
for (var i = 0; i < paramTypes.Length; i++)
|
|
{
|
|
var jsonStr = serializedParams[i];
|
|
var arrayType = paramTypes[i].MakeArrayType();
|
|
var arr = jsonStr.JsonTo(arrayType, settings) as Array;
|
|
deserializedParams[i] = arr?.GetValue(0)!;
|
|
|
|
Console.WriteLine($"ProcessOnReceiveMessage deserialized: {paramTypes[i].Name} -> {deserializedParams[i]}");
|
|
}
|
|
|
|
// Assert - All values should be correctly deserialized
|
|
Assert.IsTrue((bool)deserializedParams[0], "loadRelations should be TRUE!");
|
|
Assert.AreEqual("filterText", (string)deserializedParams[1]);
|
|
Assert.AreEqual(100, (int)deserializedParams[2]);
|
|
Assert.AreEqual(contextParams[3], (Guid)deserializedParams[3]);
|
|
Assert.AreEqual(TestStatus.Processing, (TestStatus)deserializedParams[4]);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void PrimitiveArray_GenericJsonTo_BoolArray_WorksCorrectly()
|
|
{
|
|
// Test the generic JsonTo<T> method with primitive arrays
|
|
var settings = GetMergeSettings();
|
|
|
|
var json = "[true, false, true]";
|
|
var result = json.JsonTo<bool[]>(settings);
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(3, result.Length);
|
|
Assert.IsTrue(result[0]);
|
|
Assert.IsFalse(result[1]);
|
|
Assert.IsTrue(result[2]);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void PrimitiveArray_GenericJsonTo_IntList_WorksCorrectly()
|
|
{
|
|
// Test the generic JsonTo<T> method with primitive lists
|
|
var settings = GetMergeSettings();
|
|
|
|
var json = "[1, 2, 3, 4, 5]";
|
|
var result = json.JsonTo<List<int>>(settings);
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(5, result.Count);
|
|
CollectionAssert.AreEqual(new[] { 1, 2, 3, 4, 5 }, result.ToArray());
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Cross-Serializer Compatibility Tests
|
|
|
|
[TestMethod]
|
|
public void CrossSerializer_MixedReferences_SerializeWithHybridDeserializeWithNativeNewtonsoft()
|
|
{
|
|
// Arrange: Create complex object with both IId and non-IId shared references
|
|
TestDataFactory.ResetIdCounter();
|
|
|
|
var sharedAttr = new TestSharedAttribute
|
|
{
|
|
Id = 100,
|
|
Key = "SharedKey",
|
|
Value = "SharedValue",
|
|
CreatedOrUpdatedDateUTC = DateTime.UtcNow
|
|
};
|
|
|
|
var sharedMeta = new TestNonIdMetadata
|
|
{
|
|
Key = "SharedMeta",
|
|
Value = "MetaValue",
|
|
Timestamp = DateTime.UtcNow,
|
|
ChildMetadata = new TestNonIdMetadata
|
|
{
|
|
Key = "ChildMeta",
|
|
Value = "ChildValue",
|
|
Timestamp = DateTime.UtcNow
|
|
}
|
|
};
|
|
|
|
// Shared attribute also has nested non-IId metadata
|
|
sharedAttr.NestedMetadata = sharedMeta;
|
|
|
|
var order = new TestOrder
|
|
{
|
|
Id = 1,
|
|
OrderNumber = "ORD-001",
|
|
OrderStatus = TestStatus.Processing,
|
|
CreatedAt = DateTime.UtcNow,
|
|
PrimaryAttribute = sharedAttr,
|
|
SecondaryAttribute = sharedAttr, // Same IId reference
|
|
OrderMetadata = sharedMeta,
|
|
AuditMetadata = sharedMeta, // Same non-IId reference
|
|
Items =
|
|
[
|
|
new TestOrderItem
|
|
{
|
|
Id = 10,
|
|
ProductName = "Product-A",
|
|
Quantity = 5,
|
|
UnitPrice = 10.50m,
|
|
Attribute = sharedAttr,
|
|
ItemMetadata = sharedMeta
|
|
}
|
|
],
|
|
Attributes = [sharedAttr]
|
|
};
|
|
|
|
var settings = GetMergeSettings();
|
|
settings.Formatting = Formatting.Indented;
|
|
|
|
// Act
|
|
var json = order.ToJson(settings);
|
|
|
|
Console.WriteLine("=== Serialized JSON (HybridReferenceResolver) ===");
|
|
Console.WriteLine(json);
|
|
|
|
// Verify JSON structure
|
|
Assert.IsTrue(json.Contains("\"$id\""), "Should contain $id for references");
|
|
Assert.IsTrue(json.Contains("\"$ref\""), "Should contain $ref for duplicate references");
|
|
|
|
// Count $ref occurrences - should have multiple (for both IId and non-IId duplicates)
|
|
var refCount = json.Split("\"$ref\"").Length - 1;
|
|
Console.WriteLine($"$ref count: {refCount}");
|
|
Assert.IsTrue(refCount >= 3, $"Should have at least 3 $ref tokens (shared IId and non-IId). Found: {refCount}");
|
|
|
|
// Step 2: Deserialize with native Newtonsoft (NO custom resolver, just PreserveReferencesHandling)
|
|
var nativeSettings = new JsonSerializerSettings
|
|
{
|
|
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
|
|
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
|
NullValueHandling = NullValueHandling.Ignore
|
|
};
|
|
|
|
var deserializedOrder = JsonConvert.DeserializeObject<TestOrder>(json, nativeSettings);
|
|
|
|
// Step 3: Verify deserialization
|
|
Assert.IsNotNull(deserializedOrder, "Deserialized order should not be null");
|
|
Assert.AreEqual(1, deserializedOrder.Id);
|
|
Assert.AreEqual("ORD-001", deserializedOrder.OrderNumber);
|
|
Assert.AreEqual(TestStatus.Processing, deserializedOrder.OrderStatus);
|
|
|
|
// Verify items
|
|
Assert.AreEqual(1, deserializedOrder.Items.Count);
|
|
Assert.AreEqual(10, deserializedOrder.Items[0].Id);
|
|
Assert.AreEqual("Product-A", deserializedOrder.Items[0].ProductName);
|
|
Assert.AreEqual(5, deserializedOrder.Items[0].Quantity);
|
|
Assert.AreEqual(10.50m, deserializedOrder.Items[0].UnitPrice);
|
|
|
|
// Verify shared IId references are resolved correctly
|
|
Assert.IsNotNull(deserializedOrder.PrimaryAttribute);
|
|
Assert.IsNotNull(deserializedOrder.SecondaryAttribute);
|
|
Assert.AreEqual(100, deserializedOrder.PrimaryAttribute.Id);
|
|
Assert.AreEqual("SharedKey", deserializedOrder.PrimaryAttribute.Key);
|
|
|
|
// KEY TEST: Shared IId references should be the SAME object instance
|
|
Assert.AreSame(deserializedOrder.PrimaryAttribute, deserializedOrder.SecondaryAttribute,
|
|
"PrimaryAttribute and SecondaryAttribute should be same instance (IId shared reference)");
|
|
Assert.AreSame(deserializedOrder.PrimaryAttribute, deserializedOrder.Items[0].Attribute,
|
|
"Order.PrimaryAttribute and Item.Attribute should be same instance");
|
|
Assert.AreSame(deserializedOrder.PrimaryAttribute, deserializedOrder.Attributes[0],
|
|
"Order.PrimaryAttribute and Attributes[0] should be same instance");
|
|
|
|
// Verify shared non-IId references are resolved correctly
|
|
Assert.IsNotNull(deserializedOrder.OrderMetadata);
|
|
Assert.IsNotNull(deserializedOrder.AuditMetadata);
|
|
Assert.AreEqual("SharedMeta", deserializedOrder.OrderMetadata.Key);
|
|
|
|
// KEY TEST: Shared non-IId references should be the SAME object instance
|
|
Assert.AreSame(deserializedOrder.OrderMetadata, deserializedOrder.AuditMetadata,
|
|
"OrderMetadata and AuditMetadata should be same instance (non-IId shared reference)");
|
|
Assert.AreSame(deserializedOrder.OrderMetadata, deserializedOrder.Items[0].ItemMetadata,
|
|
"Order.OrderMetadata and Item.ItemMetadata should be same instance");
|
|
|
|
// Verify nested non-IId in IId type
|
|
Assert.IsNotNull(deserializedOrder.PrimaryAttribute.NestedMetadata);
|
|
Assert.AreSame(deserializedOrder.OrderMetadata, deserializedOrder.PrimaryAttribute.NestedMetadata,
|
|
"Shared attribute's NestedMetadata should be same as OrderMetadata");
|
|
|
|
// Verify child metadata
|
|
Assert.IsNotNull(deserializedOrder.OrderMetadata.ChildMetadata);
|
|
Assert.AreEqual("ChildMeta", deserializedOrder.OrderMetadata.ChildMetadata.Key);
|
|
|
|
Console.WriteLine("=== All cross-serializer compatibility checks passed! ===");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region WASM Compatibility Tests
|
|
|
|
/// <summary>
|
|
/// Tests that verify WASM compatibility - no reflection emit, no dynamic code generation,
|
|
/// compatible with AOT compilation and interpreter mode.
|
|
/// </summary>
|
|
|
|
[TestMethod]
|
|
public void WasmCompat_AcJsonSerializer_SimpleObject_WorksWithoutReflectionEmit()
|
|
{
|
|
// Arrange - Simple object without complex inheritance
|
|
var item = new TestOrderItem
|
|
{
|
|
Id = 1,
|
|
ProductName = "Test Product",
|
|
Quantity = 10,
|
|
UnitPrice = 99.99m,
|
|
ItemStatus = TestStatus.Processing
|
|
};
|
|
|
|
// Act
|
|
var json = AcJsonSerializer.Serialize(item);
|
|
|
|
// Assert
|
|
Assert.IsNotNull(json);
|
|
Assert.IsTrue(json.Contains("\"Id\":1"));
|
|
Assert.IsTrue(json.Contains("\"ProductName\":\"Test Product\""));
|
|
Assert.IsTrue(json.Contains("\"Quantity\":10"));
|
|
Assert.IsTrue(json.Contains("\"UnitPrice\":99.99"));
|
|
Assert.IsTrue(json.Contains("\"ItemStatus\":20")); // Processing = 20
|
|
|
|
Console.WriteLine($"Serialized JSON: {json}");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void WasmCompat_AcJsonDeserializer_SimpleObject_WorksWithSystemTextJson()
|
|
{
|
|
// Arrange - JSON created by AcJsonSerializer
|
|
var original = new TestOrderItem
|
|
{
|
|
Id = 42,
|
|
ProductName = "WASM Test",
|
|
Quantity = 5,
|
|
UnitPrice = 25.50m,
|
|
ItemStatus = TestStatus.Shipped
|
|
};
|
|
var json = AcJsonSerializer.Serialize(original);
|
|
|
|
// Act
|
|
var deserialized = AcJsonDeserializer.Deserialize<TestOrderItem>(json);
|
|
|
|
// Assert
|
|
Assert.IsNotNull(deserialized);
|
|
Assert.AreEqual(42, deserialized.Id);
|
|
Assert.AreEqual("WASM Test", deserialized.ProductName);
|
|
Assert.AreEqual(5, deserialized.Quantity);
|
|
Assert.AreEqual(25.50m, deserialized.UnitPrice);
|
|
Assert.AreEqual(TestStatus.Shipped, deserialized.ItemStatus);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void WasmCompat_RoundTrip_ComplexHierarchy_PreservesAllData()
|
|
{
|
|
// Arrange - Complex 3-level hierarchy
|
|
TestDataFactory.ResetIdCounter();
|
|
var order = new TestOrder
|
|
{
|
|
Id = 1,
|
|
OrderNumber = "WASM-001",
|
|
OrderStatus = TestStatus.Processing,
|
|
CreatedAt = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc),
|
|
Items =
|
|
[
|
|
new TestOrderItem
|
|
{
|
|
Id = 10,
|
|
ProductName = "Item A",
|
|
Quantity = 3,
|
|
UnitPrice = 15.00m,
|
|
Pallets =
|
|
[
|
|
new TestPallet
|
|
{
|
|
Id = 100,
|
|
PalletCode = "PLT-A1",
|
|
TrayCount = 5,
|
|
Status = TestStatus.Pending
|
|
}
|
|
]
|
|
},
|
|
new TestOrderItem
|
|
{
|
|
Id = 20,
|
|
ProductName = "Item B",
|
|
Quantity = 7,
|
|
UnitPrice = 22.50m
|
|
}
|
|
]
|
|
};
|
|
|
|
// Act - Serialize, then deserialize
|
|
var json = AcJsonSerializer.Serialize(order);
|
|
Console.WriteLine($"JSON size: {json.Length} chars");
|
|
Console.WriteLine($"JSON: {json}");
|
|
|
|
var deserialized = AcJsonDeserializer.Deserialize<TestOrder>(json);
|
|
|
|
// Assert
|
|
Assert.IsNotNull(deserialized);
|
|
Assert.AreEqual(1, deserialized.Id);
|
|
Assert.AreEqual("WASM-001", deserialized.OrderNumber);
|
|
Assert.AreEqual(TestStatus.Processing, deserialized.OrderStatus);
|
|
Assert.AreEqual(new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc), deserialized.CreatedAt);
|
|
|
|
Assert.AreEqual(2, deserialized.Items.Count);
|
|
|
|
var item1 = deserialized.Items[0];
|
|
Assert.AreEqual(10, item1.Id);
|
|
Assert.AreEqual("Item A", item1.ProductName);
|
|
Assert.AreEqual(3, item1.Quantity);
|
|
Assert.AreEqual(15.00m, item1.UnitPrice);
|
|
Assert.AreEqual(1, item1.Pallets.Count);
|
|
Assert.AreEqual(100, item1.Pallets[0].Id);
|
|
Assert.AreEqual("PLT-A1", item1.Pallets[0].PalletCode);
|
|
|
|
var item2 = deserialized.Items[1];
|
|
Assert.AreEqual(20, item2.Id);
|
|
Assert.AreEqual("Item B", item2.ProductName);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void WasmCompat_SharedReferences_IdRefResolution_WorksCorrectly()
|
|
{
|
|
// Arrange - Object with shared references
|
|
var sharedAttr = new TestSharedAttribute
|
|
{
|
|
Id = 999,
|
|
Key = "SharedKey",
|
|
Value = "SharedValue",
|
|
CreatedOrUpdatedDateUTC = DateTime.UtcNow
|
|
};
|
|
|
|
var order = new TestOrder
|
|
{
|
|
Id = 1,
|
|
OrderNumber = "REF-TEST",
|
|
PrimaryAttribute = sharedAttr,
|
|
SecondaryAttribute = sharedAttr, // Same reference!
|
|
Attributes = [sharedAttr]
|
|
};
|
|
|
|
// Act
|
|
var json = AcJsonSerializer.Serialize(order);
|
|
Console.WriteLine($"JSON with refs: {json}");
|
|
|
|
// Assert - JSON should have $id and $ref tokens
|
|
Assert.IsTrue(json.Contains("\"$id\""), "Should contain $id for shared reference");
|
|
Assert.IsTrue(json.Contains("\"$ref\""), "Should contain $ref for duplicate references");
|
|
|
|
// Step 2: Deserialize with native Newtonsoft (NO custom resolver, just PreserveReferencesHandling)
|
|
var nativeSettings = new JsonSerializerSettings
|
|
{
|
|
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
|
|
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
|
NullValueHandling = NullValueHandling.Ignore
|
|
};
|
|
|
|
var deserializedOrder = JsonConvert.DeserializeObject<TestOrder>(json, nativeSettings);
|
|
|
|
// Step 3: Verify deserialization
|
|
Assert.IsNotNull(deserializedOrder, "Deserialized order should not be null");
|
|
Assert.AreEqual(1, deserializedOrder.Id);
|
|
Assert.AreEqual("REF-TEST", deserializedOrder.OrderNumber);
|
|
|
|
// Shared references resolution
|
|
Assert.IsNotNull(deserializedOrder.PrimaryAttribute);
|
|
Assert.IsNotNull(deserializedOrder.SecondaryAttribute);
|
|
Assert.AreEqual(999, deserializedOrder.PrimaryAttribute.Id);
|
|
Assert.AreEqual("SharedKey", deserializedOrder.PrimaryAttribute.Key);
|
|
|
|
// KEY TEST: Shared references should be resolved to the same object
|
|
Assert.AreSame(deserializedOrder.PrimaryAttribute, deserializedOrder.SecondaryAttribute,
|
|
"Shared references should resolve to same object instance");
|
|
Assert.AreSame(deserializedOrder.PrimaryAttribute, deserializedOrder.Attributes[0],
|
|
"Collection reference should resolve to same object instance");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void WasmCompat_AllPrimitiveTypes_SerializeAndDeserialize()
|
|
{
|
|
// Arrange - Test all primitive types that might behave differently in WASM
|
|
var testData = new WasmPrimitiveTestClass
|
|
{
|
|
IntValue = int.MaxValue,
|
|
LongValue = long.MaxValue,
|
|
DoubleValue = 3.14159265358979,
|
|
DecimalValue = 12345.6789m,
|
|
FloatValue = 1.5f,
|
|
BoolValue = true,
|
|
StringValue = "Hello WASM! 🚀 Unicode: αβγδ",
|
|
GuidValue = Guid.Parse("12345678-1234-1234-1234-123456789abc"),
|
|
DateTimeValue = new DateTime(2024, 12, 25, 12, 30, 45, DateTimeKind.Utc),
|
|
EnumValue = TestStatus.Shipped,
|
|
ByteValue = 255,
|
|
ShortValue = short.MaxValue,
|
|
NullableInt = 42,
|
|
NullableIntNull = null
|
|
};
|
|
|
|
// Act
|
|
var json = AcJsonSerializer.Serialize(testData);
|
|
Console.WriteLine($"Primitives JSON: {json}");
|
|
|
|
var deserialized = AcJsonDeserializer.Deserialize<WasmPrimitiveTestClass>(json);
|
|
|
|
// Assert
|
|
Assert.IsNotNull(deserialized);
|
|
Assert.AreEqual(int.MaxValue, deserialized.IntValue);
|
|
Assert.AreEqual(long.MaxValue, deserialized.LongValue);
|
|
Assert.AreEqual(3.14159265358979, deserialized.DoubleValue, 0.0000000001);
|
|
Assert.AreEqual(12345.6789m, deserialized.DecimalValue);
|
|
Assert.AreEqual(1.5f, deserialized.FloatValue);
|
|
Assert.IsTrue(deserialized.BoolValue);
|
|
Assert.AreEqual("Hello WASM! 🚀 Unicode: αβγδ", deserialized.StringValue);
|
|
Assert.AreEqual(Guid.Parse("12345678-1234-1234-1234-123456789abc"), deserialized.GuidValue);
|
|
Assert.AreEqual(new DateTime(2024, 12, 25, 12, 30, 45, DateTimeKind.Utc), deserialized.DateTimeValue);
|
|
Assert.AreEqual(TestStatus.Shipped, deserialized.EnumValue);
|
|
Assert.AreEqual(255, deserialized.ByteValue);
|
|
Assert.AreEqual(short.MaxValue, deserialized.ShortValue);
|
|
Assert.AreEqual(42, deserialized.NullableInt);
|
|
Assert.IsNull(deserialized.NullableIntNull);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test class with all primitive types for WASM compatibility testing
|
|
/// </summary>
|
|
public class WasmPrimitiveTestClass
|
|
{
|
|
public int IntValue { get; set; }
|
|
public long LongValue { get; set; }
|
|
public double DoubleValue { get; set; }
|
|
public decimal DecimalValue { get; set; }
|
|
public float FloatValue { get; set; }
|
|
public bool BoolValue { get; set; }
|
|
public string StringValue { get; set; } = "";
|
|
public Guid GuidValue { get; set; }
|
|
public DateTime DateTimeValue { get; set; }
|
|
public TestStatus EnumValue { get; set; }
|
|
public byte ByteValue { get; set; }
|
|
public short ShortValue { get; set; }
|
|
public int? NullableInt { get; set; }
|
|
public int? NullableIntNull { get; set; }
|
|
}
|
|
|
|
[TestMethod]
|
|
public void WasmCompat_EmptyAndNullCollections_HandleCorrectly()
|
|
{
|
|
// Arrange
|
|
var order = new TestOrder
|
|
{
|
|
Id = 1,
|
|
OrderNumber = "EMPTY-TEST",
|
|
Items = [], // Empty list
|
|
Attributes = [], // Empty list
|
|
MetadataList = [] // Empty list
|
|
};
|
|
|
|
// Act
|
|
var json = AcJsonSerializer.Serialize(order);
|
|
Console.WriteLine($"Empty collections JSON: {json}");
|
|
|
|
// Empty collections SHOULD be in JSON as [] (not omitted)
|
|
// This preserves the distinction between null and empty
|
|
Assert.IsTrue(json.Contains("\"Items\":[]"), "Empty Items should be serialized as []");
|
|
Assert.IsTrue(json.Contains("\"Attributes\":[]"), "Empty Attributes should be serialized as []");
|
|
|
|
var deserialized = AcJsonDeserializer.Deserialize<TestOrder>(json);
|
|
|
|
// Assert - deserialized object should have empty (not null) collections
|
|
Assert.IsNotNull(deserialized);
|
|
Assert.IsNotNull(deserialized.Items, "Items should be an empty list, not null after deserialization");
|
|
Assert.AreEqual(0, deserialized.Items.Count);
|
|
Assert.IsNotNull(deserialized.Attributes, "Attributes should be an empty list, not null after deserialization");
|
|
Assert.AreEqual(0, deserialized.Attributes.Count);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Serialize_NullCollection_IsOmitted()
|
|
{
|
|
// Arrange - Order with null collections
|
|
var order = new TestOrderWithNullableCollections
|
|
{
|
|
Id = 1,
|
|
OrderNumber = "TEST-001",
|
|
Items = null, // Explicitly null
|
|
Tags = null // Explicitly null
|
|
};
|
|
|
|
// Act
|
|
var json = AcJsonSerializer.Serialize(order);
|
|
Console.WriteLine($"JSON with null collections: {json}");
|
|
|
|
// Assert - Null collections should NOT be in JSON
|
|
Assert.IsFalse(json.Contains("\"Items\""),
|
|
$"Null Items should not be serialized. JSON: {json}");
|
|
Assert.IsFalse(json.Contains("\"Tags\""),
|
|
$"Null Tags should not be serialized. JSON: {json}");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Deserialize_EmptyArray_CreatesEmptyList()
|
|
{
|
|
// Arrange - JSON with empty Items array
|
|
var json = """{"Id":1,"OrderNumber":"TEST-001","Items":[],"Attributes":[]}""";
|
|
|
|
// Act
|
|
var order = AcJsonDeserializer.Deserialize<TestOrder>(json);
|
|
|
|
// Assert
|
|
Assert.IsNotNull(order);
|
|
Assert.IsNotNull(order.Items, "Items should be an empty list, not null after deserialization");
|
|
Assert.AreEqual(0, order.Items.Count);
|
|
Assert.IsNotNull(order.Attributes, "Attributes should be an empty list, not null after deserialization");
|
|
Assert.AreEqual(0, order.Attributes.Count);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Deserialize_MissingCollection_StaysAsInitialized()
|
|
{
|
|
// Arrange - JSON without Items or Attributes (simulates old serialization)
|
|
var json = """{"Id":1,"OrderNumber":"TEST-001"}""";
|
|
|
|
// Act
|
|
var order = AcJsonDeserializer.Deserialize<TestOrder>(json);
|
|
|
|
// Assert - The DTO initializes these as empty lists in constructor
|
|
// Note: This depends on the class having initialized properties like: Items = [];
|
|
// If missing from JSON, they should retain their default initialized value
|
|
Assert.IsNotNull(order);
|
|
Assert.IsNotNull(order.Items, "Items should retain default empty list initialization");
|
|
Assert.IsNotNull(order.Attributes, "Attributes should retain default empty list initialization");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void RoundTrip_EmptyCollection_PreservesEmptyNotNull()
|
|
{
|
|
// Arrange
|
|
var original = new TestOrder
|
|
{
|
|
Id = 1,
|
|
OrderNumber = "TEST-001",
|
|
Items = [],
|
|
Attributes = []
|
|
};
|
|
|
|
// Act - Serialize then deserialize
|
|
var json = AcJsonSerializer.Serialize(original);
|
|
Console.WriteLine($"Round-trip JSON: {json}");
|
|
|
|
var deserialized = AcJsonDeserializer.Deserialize<TestOrder>(json);
|
|
|
|
// Assert
|
|
Assert.IsNotNull(deserialized);
|
|
Assert.IsNotNull(deserialized.Items, "Items should be empty list, not null after round-trip");
|
|
Assert.AreEqual(0, deserialized.Items.Count);
|
|
Assert.IsNotNull(deserialized.Attributes, "Attributes should be empty list, not null after round-trip");
|
|
Assert.AreEqual(0, deserialized.Attributes.Count);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test class with nullable collections to test null vs empty distinction
|
|
/// </summary>
|
|
public class TestOrderWithNullableCollections
|
|
{
|
|
public int Id { get; set; }
|
|
public string OrderNumber { get; set; } = "";
|
|
public List<TestOrderItem>? Items { get; set; }
|
|
public List<string>? Tags { get; set; }
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IdMessage SignalR Tests (Reproduces ProcessOnReceiveMessage behavior)
|
|
|
|
/// <summary>
|
|
/// These tests reproduce the exact flow in AcWebSignalRHubBase.ProcessOnReceiveMessage
|
|
/// where IdMessage wraps parameters and the server deserializes them back.
|
|
/// </summary>
|
|
|
|
[TestMethod]
|
|
public void IdMessage_FullSignalRScenario_IntArrayParameter()
|
|
{
|
|
// Reproduces: GetAllOrderDtoByIds(int[] orderIds)
|
|
// The error in logs: "Cannot deserialize the current JSON object into type 'System.Int32[]'"
|
|
|
|
var orderIds = new int[] { 113, 456, 789 };
|
|
|
|
// Step 1: Client side - IdMessage constructor wraps the parameter
|
|
// In IdMessage: item = (new[] { x }).ToJson() where x is int[]
|
|
var clientSideJson = (new[] { orderIds }).ToJson();
|
|
Console.WriteLine($"Client IdMessage.Ids[0] = \"{clientSideJson}\"");
|
|
|
|
// Expected: "[[113,456,789]]" - array containing an int array
|
|
Assert.IsTrue(clientSideJson.StartsWith("[[") && clientSideJson.EndsWith("]]"),
|
|
$"Should be nested array format. Got: {clientSideJson}");
|
|
|
|
// Step 2: Server side - ProcessOnReceiveMessage deserializes
|
|
// Code: var a = Array.CreateInstance(methodInfoModel.ParamInfos[i].ParameterType, 1);
|
|
// paramValues[i] = (obj.JsonTo(a.GetType()) as Array)?.GetValue(0)!;
|
|
|
|
var parameterType = typeof(int[]); // The method parameter type
|
|
var wrapperArrayType = parameterType.MakeArrayType(); // int[][] to wrap it
|
|
|
|
// Deserialize using the same settings as SignalR
|
|
var settings = GetMergeSettings();
|
|
var deserializedWrapper = clientSideJson.JsonTo(wrapperArrayType, settings) as Array;
|
|
|
|
Assert.IsNotNull(deserializedWrapper, $"Should deserialize to int[][]. JSON: {clientSideJson}");
|
|
Assert.AreEqual(1, deserializedWrapper.Length);
|
|
|
|
var actualParameter = deserializedWrapper.GetValue(0) as int[];
|
|
Assert.IsNotNull(actualParameter, "Should get int[] from wrapper");
|
|
Assert.AreEqual(3, actualParameter.Length);
|
|
Assert.AreEqual(113, actualParameter[0]);
|
|
Assert.AreEqual(456, actualParameter[1]);
|
|
Assert.AreEqual(789, actualParameter[2]);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void IdMessage_FullSignalRScenario_SingleIntParameter()
|
|
{
|
|
// Reproduces: GetOrderById(int orderId)
|
|
var orderId = 42;
|
|
|
|
// Client: (new[] { x }).ToJson()
|
|
var clientSideJson = (new[] { orderId }).ToJson();
|
|
Console.WriteLine($"Client IdMessage.Ids[0] = \"{clientSideJson}\"");
|
|
|
|
// Expected: "[42]"
|
|
Assert.AreEqual("[42]", clientSideJson);
|
|
|
|
// Server side deserialization
|
|
var parameterType = typeof(int);
|
|
var wrapperArrayType = parameterType.MakeArrayType(); // int[]
|
|
|
|
var settings = GetMergeSettings();
|
|
var deserializedArray = clientSideJson.JsonTo(wrapperArrayType, settings) as Array;
|
|
|
|
Assert.IsNotNull(deserializedArray);
|
|
Assert.AreEqual(1, deserializedArray.Length);
|
|
Assert.AreEqual(42, deserializedArray.GetValue(0));
|
|
}
|
|
|
|
[TestMethod]
|
|
public void IdMessage_FullSignalRScenario_BoolParameter()
|
|
{
|
|
// Reproduces: GetPendingOrders(bool loadRelations)
|
|
// This was the original bug - loadRelations=true becoming false
|
|
|
|
var loadRelations = true;
|
|
|
|
// Client: (new[] { x }).ToJson()
|
|
var clientSideJson = (new[] { loadRelations }).ToJson();
|
|
Console.WriteLine($"Client IdMessage.Ids[0] = \"{clientSideJson}\"");
|
|
|
|
// Expected: "[true]"
|
|
Assert.AreEqual("[true]", clientSideJson);
|
|
|
|
// Server side deserialization
|
|
var parameterType = typeof(bool);
|
|
var wrapperArrayType = parameterType.MakeArrayType(); // bool[]
|
|
|
|
var settings = GetMergeSettings();
|
|
var deserializedArray = clientSideJson.JsonTo(wrapperArrayType, settings) as Array;
|
|
|
|
Assert.IsNotNull(deserializedArray);
|
|
Assert.AreEqual(1, deserializedArray.Length);
|
|
Assert.IsTrue((bool)deserializedArray.GetValue(0)!, "loadRelations should remain TRUE!");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void IdMessage_FullSignalRScenario_GuidArrayParameter()
|
|
{
|
|
// Reproduces: GetOrdersByIds(Guid[] orderIds)
|
|
var guid1 = Guid.NewGuid();
|
|
var guid2 = Guid.NewGuid();
|
|
var orderIds = new Guid[] { guid1, guid2 };
|
|
|
|
// Client: (new[] { x }).ToJson()
|
|
var clientSideJson = (new[] { orderIds }).ToJson();
|
|
Console.WriteLine($"Client IdMessage.Ids[0] = \"{clientSideJson}\"");
|
|
|
|
// Server side deserialization
|
|
var parameterType = typeof(Guid[]);
|
|
var wrapperArrayType = parameterType.MakeArrayType(); // Guid[][]
|
|
|
|
var settings = GetMergeSettings();
|
|
var deserializedWrapper = clientSideJson.JsonTo(wrapperArrayType, settings) as Array;
|
|
|
|
Assert.IsNotNull(deserializedWrapper);
|
|
Assert.AreEqual(1, deserializedWrapper.Length);
|
|
|
|
var actualParameter = deserializedWrapper.GetValue(0) as Guid[];
|
|
Assert.IsNotNull(actualParameter);
|
|
Assert.AreEqual(2, actualParameter.Length);
|
|
Assert.AreEqual(guid1, actualParameter[0]);
|
|
Assert.AreEqual(guid2, actualParameter[1]);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void IdMessage_FullSignalRScenario_MultipleParameters()
|
|
{
|
|
// Reproduces: GetFilteredOrders(bool loadRelations, string filter, int pageSize, Guid userId)
|
|
// This simulates the full ProcessOnReceiveMessage loop
|
|
|
|
var settings = GetMergeSettings();
|
|
|
|
// Define method parameters
|
|
var methodParams = new (Type type, object value)[]
|
|
{
|
|
(typeof(bool), true),
|
|
(typeof(string), "active"),
|
|
(typeof(int), 50),
|
|
(typeof(Guid), Guid.NewGuid()),
|
|
(typeof(TestStatus), TestStatus.Processing)
|
|
};
|
|
|
|
// Client side: IdMessage wraps each parameter
|
|
var serializedParams = new List<string>();
|
|
foreach (var (type, value) in methodParams)
|
|
{
|
|
// This is what IdMessage does: item = (new[] { x }).ToJson();
|
|
var wrapped = Array.CreateInstance(type, 1);
|
|
wrapped.SetValue(value, 0);
|
|
var json = wrapped.ToJson(settings);
|
|
serializedParams.Add(json);
|
|
Console.WriteLine($"Param {type.Name}: {value} -> {json}");
|
|
}
|
|
|
|
// Server side: ProcessOnReceiveMessage deserializes each parameter
|
|
var deserializedParams = new object[methodParams.Length];
|
|
for (var i = 0; i < methodParams.Length; i++)
|
|
{
|
|
var paramType = methodParams[i].type;
|
|
var jsonStr = serializedParams[i];
|
|
|
|
// This is the server-side code:
|
|
// var a = Array.CreateInstance(paramType, 1);
|
|
// paramValues[i] = (obj.JsonTo(a.GetType()) as Array)?.GetValue(0)!;
|
|
var arrayType = paramType.MakeArrayType();
|
|
var arr = jsonStr.JsonTo(arrayType, settings) as Array;
|
|
deserializedParams[i] = arr?.GetValue(0)!;
|
|
|
|
Console.WriteLine($"Deserialized {paramType.Name}: {deserializedParams[i]}");
|
|
}
|
|
|
|
// Assert all parameters are correctly deserialized
|
|
Assert.IsTrue((bool)deserializedParams[0], "loadRelations should be TRUE");
|
|
Assert.AreEqual("active", (string)deserializedParams[1]);
|
|
Assert.AreEqual(50, (int)deserializedParams[2]);
|
|
Assert.AreEqual(methodParams[3].value, (Guid)deserializedParams[3]);
|
|
Assert.AreEqual(TestStatus.Processing, (TestStatus)deserializedParams[4]);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void IdMessage_FullSignalRScenario_ComplexObjectParameter()
|
|
{
|
|
// Reproduces: UpdateOrder(OrderDto order)
|
|
var order = new TestOrder
|
|
{
|
|
Id = 1,
|
|
OrderNumber = "ORD-001",
|
|
OrderStatus = TestStatus.Processing,
|
|
Items =
|
|
[
|
|
new TestOrderItem { Id = 10, ProductName = "Product A", Quantity = 5 }
|
|
]
|
|
};
|
|
|
|
// Client: (new[] { x }).ToJson()
|
|
var clientSideJson = (new[] { order }).ToJson();
|
|
Console.WriteLine($"Client IdMessage.Ids[0] = \"{clientSideJson}\"");
|
|
|
|
// Server side deserialization
|
|
var parameterType = typeof(TestOrder);
|
|
var wrapperArrayType = parameterType.MakeArrayType(); // TestOrder[]
|
|
|
|
var settings = GetMergeSettings();
|
|
var deserializedWrapper = clientSideJson.JsonTo(wrapperArrayType, settings) as Array;
|
|
|
|
Assert.IsNotNull(deserializedWrapper);
|
|
Assert.AreEqual(1, deserializedWrapper.Length);
|
|
|
|
var actualParameter = deserializedWrapper.GetValue(0) as TestOrder;
|
|
Assert.IsNotNull(actualParameter);
|
|
Assert.AreEqual(1, actualParameter.Id);
|
|
Assert.AreEqual("ORD-001", actualParameter.OrderNumber);
|
|
Assert.AreEqual(TestStatus.Processing, actualParameter.OrderStatus);
|
|
Assert.AreEqual(1, actualParameter.Items.Count);
|
|
Assert.AreEqual("Product A", actualParameter.Items[0].ProductName);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void IdMessage_CompareSerializers_IntArray()
|
|
{
|
|
// Ensure AcJsonSerializer and Newtonsoft produce identical output
|
|
var orderIds = new int[] { 1, 2, 3 };
|
|
var wrapped = new[] { orderIds };
|
|
|
|
// AcJsonSerializer (new default)
|
|
var acJson = AcJsonSerializer.Serialize(wrapped);
|
|
|
|
// Newtonsoft (old default)
|
|
var newtonsoftJson = JsonConvert.SerializeObject(wrapped);
|
|
|
|
Console.WriteLine($"AcJsonSerializer: {acJson}");
|
|
Console.WriteLine($"Newtonsoft: {newtonsoftJson}");
|
|
|
|
// Both should produce: [[1,2,3]]
|
|
Assert.AreEqual(newtonsoftJson, acJson,
|
|
"AcJsonSerializer should produce same output as Newtonsoft for primitive arrays");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void IdMessage_CompareSerializers_NestedArray()
|
|
{
|
|
// Test nested arrays (edge case)
|
|
var matrix = new int[][] { new[] { 1, 2 }, new[] { 3, 4 } };
|
|
var wrapped = new[] { matrix };
|
|
|
|
var acJson = AcJsonSerializer.Serialize(wrapped);
|
|
var newtonsoftJson = JsonConvert.SerializeObject(wrapped);
|
|
|
|
Console.WriteLine($"AcJsonSerializer: {acJson}");
|
|
Console.WriteLine($"Newtonsoft: {newtonsoftJson}");
|
|
|
|
// Both should produce: [[[1,2],[3,4]]]
|
|
Assert.AreEqual(newtonsoftJson, acJson);
|
|
}
|
|
|
|
#endregion
|
|
} |