AyCode.Core/AyCode.Core.Tests/JsonExtensionTests.cs

1190 lines
45 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 TypeName_Id format)
[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: Semantic $id format for IId types
Assert.IsTrue(json.Contains($"TestOrder_{order.Id}"), $"Should contain semantic $id for TestOrder. JSON:\n{json}");
Assert.IsTrue(json.Contains($"TestOrderItem_{order.Items[0].Id}"), $"Should contain semantic $id for TestOrderItem. JSON:\n{json}");
Assert.IsTrue(json.Contains($"TestSharedAttribute_{sharedAttr.Id}"), $"Should contain semantic $id for TestSharedAttribute. 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}");
}
[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 numeric $ref for non-IId duplicates
Assert.IsTrue(json.Contains("\"$ref\""), $"Should contain Newtonsoft $ref for shared non-IId metadata. JSON:\n{json}");
// Assert: Semantic IId references also present
Assert.IsTrue(json.Contains($"TestOrder_{order.Id}"), "Should also contain semantic $id for IId types");
}
[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: Both reference systems work
Assert.IsTrue(json.Contains("TestOrder_1"), "Should have semantic $id for TestOrder");
Assert.IsTrue(json.Contains("TestOrderItem_10"), "Should have semantic $id for TestOrderItem");
Assert.IsTrue(json.Contains($"TestSharedAttribute_{sharedAttr.Id}"), "Should have semantic $id for TestSharedAttribute");
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 - semantic IId references
Assert.IsTrue(json.Contains($"TestOrder_{originalOrder.Id}"), "Should have semantic $id for root order");
Assert.IsTrue(json.Contains($"TestOrderItem_{originalOrder.Items[0].Id}"), "Should have semantic $id for items");
Assert.IsTrue(json.Contains($"TestPallet_{originalOrder.Items[0].Pallets[0].Id}"), "Should have semantic $id for pallets");
Assert.IsTrue(json.Contains($"TestMeasurement_{originalOrder.Items[0].Pallets[0].Measurements[0].Id}"), "Should have semantic $id for measurements");
Assert.IsTrue(json.Contains($"TestMeasurementPoint_{originalOrder.Items[0].Pallets[0].Measurements[0].Points[0].Id}"), "Should have semantic $id for points");
// 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");
}
[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()
{
var filterText = "test filter";
var settings = GetMergeSettings();
var jsonString = (new[] { filterText }).ToJson(settings);
Console.WriteLine($"Serialized [\"test filter\"]: {jsonString}");
var targetArrayType = typeof(string).MakeArrayType();
var deserializedArray = jsonString.JsonTo(targetArrayType, settings) as Array;
var deserializedValue = deserializedArray?.GetValue(0);
Console.WriteLine($"Deserialized value: {deserializedValue}");
Assert.IsNotNull(deserializedArray);
Assert.AreEqual("test filter", (string)deserializedValue!, "String should deserialize correctly");
}
[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
}