diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs new file mode 100644 index 0000000..c9f8976 --- /dev/null +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs @@ -0,0 +1,468 @@ +using AyCode.Core.Extensions; +using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Tests.TestModels; + +namespace AyCode.Core.Tests.Serialization; + +/// +/// Tests for IId-based reference handling in Binary serializer. +/// Two scenarios: +/// 1. Same instance referenced multiple times (object identity) +/// 2. Different instances with same IId.Id (IId-based deduplication) +/// +/// Tests verify BOTH: +/// - Serialized output uses ObjectRef (not redundant full objects) +/// - Deserialized result maintains reference identity +/// +[TestClass] +public class AcBinarySerializerIIdReferenceTests +{ + // BinaryTypeCode.ObjectRef = 27 + private const byte ObjectRefTypeCode = 27; + + #region Helper Methods + + /// + /// Counts occurrences of ObjectRef (0x1B = 27) in binary data. + /// + private static int CountObjectRefs(byte[] binary) + { + var count = 0; + for (var i = 0; i < binary.Length; i++) + { + if (binary[i] == ObjectRefTypeCode) + count++; + } + return count; + } + + /// + /// Counts occurrences of a string in binary data (UTF8). + /// + private static int CountStringOccurrences(byte[] binary, string searchString) + { + var searchBytes = System.Text.Encoding.UTF8.GetBytes(searchString); + var count = 0; + for (var i = 0; i <= binary.Length - searchBytes.Length; i++) + { + var match = true; + for (var j = 0; j < searchBytes.Length; j++) + { + if (binary[i + j] != searchBytes[j]) + { + match = false; + break; + } + } + if (match) count++; + } + return count; + } + + #endregion + + #region Scenario 1: Same Instance (Object Identity) + + /// + /// SCENARIO 1: Same instance referenced multiple times. + /// Validates: ObjectRef present + data integrity after deserialize + reference identity. + /// + [TestMethod] + public void SameInstance_SerializeAndDeserialize() + { + // Arrange: SAME instance used 4 times + var sharedTag = new SharedTag { Id = 1, Name = "ImportantTag", Color = "#FF0000" }; + + var order = new TestOrder + { + Id = 1, + OrderNumber = "ORD-001", + PrimaryTag = sharedTag, + Items = + [ + new TestOrderItem { Id = 1, ProductName = "Product-A", Tag = sharedTag }, + new TestOrderItem { Id = 2, ProductName = "Product-B", Tag = sharedTag }, + new TestOrderItem { Id = 3, ProductName = "Product-C", Tag = sharedTag } + ] + }; + + // Act + var binary = order.ToBinary(); + var result = binary.BinaryTo(); + + // Assert 1: Binary should have ObjectRef entries (reference handling is active) + var objectRefCount = CountObjectRefs(binary); + Console.WriteLine($"Binary size: {binary.Length} bytes"); + Console.WriteLine($"ObjectRef count: {objectRefCount}"); + + Assert.IsTrue(objectRefCount >= 3, + $"Expected at least 3 ObjectRef entries for shared tag, found {objectRefCount}. " + + "Reference handling may not be active!"); + + // Assert 2: Data integrity - ALL tags are present and have correct data + Assert.IsNotNull(result, "Deserialized result is null"); + Assert.IsNotNull(result.PrimaryTag, "PrimaryTag is null - data lost!"); + Assert.AreEqual(1, result.PrimaryTag.Id, "PrimaryTag.Id incorrect"); + Assert.AreEqual("ImportantTag", result.PrimaryTag.Name, "PrimaryTag.Name incorrect"); + Assert.AreEqual("#FF0000", result.PrimaryTag.Color, "PrimaryTag.Color incorrect"); + + Assert.IsNotNull(result.Items, "Items is null"); + Assert.AreEqual(3, result.Items.Count, "Items count incorrect"); + + for (var i = 0; i < 3; i++) + { + Assert.IsNotNull(result.Items[i].Tag, $"Items[{i}].Tag is null - data lost!"); + Assert.AreEqual(1, result.Items[i].Tag!.Id, $"Items[{i}].Tag.Id incorrect"); + Assert.AreEqual("ImportantTag", result.Items[i].Tag.Name, $"Items[{i}].Tag.Name incorrect"); + } + + // Assert 3: Reference identity - all should be same object reference + Assert.AreSame(result.PrimaryTag, result.Items[0].Tag, + "Item[0].Tag should be same reference as PrimaryTag"); + Assert.AreSame(result.PrimaryTag, result.Items[1].Tag, + "Item[1].Tag should be same reference as PrimaryTag"); + Assert.AreSame(result.PrimaryTag, result.Items[2].Tag, + "Item[2].Tag should be same reference as PrimaryTag"); + } + + #endregion + + #region Scenario 2: Different Instances with Same IId (IId-Based Deduplication) + + /// + /// SCENARIO 2: DIFFERENT instances with SAME IId.Id value. + /// CRITICAL test - if IId-based deduplication works: + /// - ObjectRef should be used in binary + /// - Data should be complete after deserialize + /// - References should be identical (AreSame) + /// - Different TYPES with same int Id should NOT be confused! + /// + [TestMethod] + public void DifferentInstances_SameIId_SerializeAndDeserialize() + { + // Arrange: DIFFERENT instances but SAME IId.Id + // CRITICAL: Multiple DIFFERENT TYPES all have Id=1 - must not be confused! + var order = new TestOrder + { + Id = 1, + OrderNumber = "ORD-001", + // All three types have Id=1 - tests (Type, Id) keying, not just Id + PrimaryTag = new SharedTag { Id = 1, Name = "Tag_Id1", Color = "#FF0000" }, + Owner = new SharedUser { Id = 1, Username = "User_Id1", Email = "user1@test.com" }, + Category = new SharedCategory { Id = 1, Name = "Category_Id1", SortOrder = 10 }, + Items = + [ + new TestOrderItem + { + Id = 1, + ProductName = "Product-A", + Tag = new SharedTag { Id = 1, Name = "Tag_Id1", Color = "#FF0000" }, + Assignee = new SharedUser { Id = 1, Username = "User_Id1", Email = "user1@test.com" } + }, + new TestOrderItem + { + Id = 2, + ProductName = "Product-B", + Tag = new SharedTag { Id = 1, Name = "Tag_Id1", Color = "#FF0000" }, + Assignee = new SharedUser { Id = 1, Username = "User_Id1", Email = "user1@test.com" } + }, + new TestOrderItem + { + Id = 3, + ProductName = "Product-C", + Tag = new SharedTag { Id = 1, Name = "Tag_Id1", Color = "#FF0000" }, + Assignee = new SharedUser { Id = 1, Username = "User_Id1", Email = "user1@test.com" } + } + ] + }; + + // Act + var binary = order.ToBinary(); + var result = binary.BinaryTo(); + + // Assert 1: Check if ObjectRef is used (IId-based deduplication active) + var objectRefCount = CountObjectRefs(binary); + Console.WriteLine($"Binary size: {binary.Length} bytes"); + Console.WriteLine($"ObjectRef count: {objectRefCount}"); + + // 4 Tags, 4 Users - each should have 3 ObjectRefs = 6 total minimum + Assert.IsTrue(objectRefCount >= 6, + $"CRITICAL: Expected at least 6 ObjectRef entries (3 per type for Tag and User), found {objectRefCount}. " + + "IId-based reference deduplication is NOT working!"); + + // Assert 2: Data integrity - ALL data present and correct + Assert.IsNotNull(result, "Deserialized result is null"); + + // Tag data + Assert.IsNotNull(result.PrimaryTag, "PrimaryTag is null - data lost!"); + Assert.AreEqual(1, result.PrimaryTag.Id, "PrimaryTag.Id incorrect"); + Assert.AreEqual("Tag_Id1", result.PrimaryTag.Name, "PrimaryTag.Name incorrect - might be confused with User!"); + Assert.AreEqual("#FF0000", result.PrimaryTag.Color, "PrimaryTag.Color incorrect"); + + // User data - MUST NOT be confused with Tag (both have Id=1) + Assert.IsNotNull(result.Owner, "Owner is null - data lost!"); + Assert.AreEqual(1, result.Owner.Id, "Owner.Id incorrect"); + Assert.AreEqual("User_Id1", result.Owner.Username, "Owner.Username incorrect - might be confused with Tag!"); + Assert.AreEqual("user1@test.com", result.Owner.Email, "Owner.Email incorrect"); + + // Category data - MUST NOT be confused with Tag or User (all have Id=1) + Assert.IsNotNull(result.Category, "Category is null - data lost!"); + Assert.AreEqual(1, result.Category.Id, "Category.Id incorrect"); + Assert.AreEqual("Category_Id1", result.Category.Name, "Category.Name incorrect - might be confused with Tag!"); + Assert.AreEqual(10, result.Category.SortOrder, "Category.SortOrder incorrect"); + + Assert.IsNotNull(result.Items, "Items is null"); + Assert.AreEqual(3, result.Items.Count, "Items count incorrect"); + + for (var i = 0; i < 3; i++) + { + // Tag in items + Assert.IsNotNull(result.Items[i].Tag, $"Items[{i}].Tag is null - data lost!"); + Assert.AreEqual(1, result.Items[i].Tag!.Id, $"Items[{i}].Tag.Id incorrect"); + Assert.AreEqual("Tag_Id1", result.Items[i].Tag.Name, $"Items[{i}].Tag.Name incorrect - confused with User?"); + + // User in items - MUST NOT be confused with Tag + Assert.IsNotNull(result.Items[i].Assignee, $"Items[{i}].Assignee is null - data lost!"); + Assert.AreEqual(1, result.Items[i].Assignee!.Id, $"Items[{i}].Assignee.Id incorrect"); + Assert.AreEqual("User_Id1", result.Items[i].Assignee.Username, $"Items[{i}].Assignee.Username incorrect - confused with Tag?"); + } + + // Assert 3: Reference identity - same TYPE with same Id should be same reference + // Tags with Id=1 should all be same reference + Assert.AreSame(result.PrimaryTag, result.Items[0].Tag, + "CRITICAL: Item[0].Tag should be same reference as PrimaryTag (same SharedTag.Id=1)"); + Assert.AreSame(result.PrimaryTag, result.Items[1].Tag, + "CRITICAL: Item[1].Tag should be same reference as PrimaryTag (same SharedTag.Id=1)"); + Assert.AreSame(result.PrimaryTag, result.Items[2].Tag, + "CRITICAL: Item[2].Tag should be same reference as PrimaryTag (same SharedTag.Id=1)"); + + // Users with Id=1 should all be same reference + Assert.AreSame(result.Owner, result.Items[0].Assignee, + "CRITICAL: Item[0].Assignee should be same reference as Owner (same SharedUser.Id=1)"); + Assert.AreSame(result.Owner, result.Items[1].Assignee, + "CRITICAL: Item[1].Assignee should be same reference as Owner (same SharedUser.Id=1)"); + Assert.AreSame(result.Owner, result.Items[2].Assignee, + "CRITICAL: Item[2].Assignee should be same reference as Owner (same SharedUser.Id=1)"); + + // Assert 4: Different TYPES with same Id should NOT be same reference! + Assert.AreNotSame(result.PrimaryTag, result.Owner, + "CRITICAL BUG: Tag and User are same reference! Types with same int Id were confused!"); + Assert.AreNotSame(result.PrimaryTag, result.Category, + "CRITICAL BUG: Tag and Category are same reference! Types with same int Id were confused!"); + Assert.AreNotSame(result.Owner, result.Category, + "CRITICAL BUG: User and Category are same reference! Types with same int Id were confused!"); + } + + /// + /// Size comparison: Same IId should result in smaller binary + verify data integrity. + /// + [TestMethod] + public void DifferentInstances_SameIId_SmallerBinaryWithDataIntegrity() + { + // Arrange: 10 different instances with SAME IId + var orderWithSameIId = new TestOrder + { + Id = 1, + OrderNumber = "SAME-IID", + Items = Enumerable.Range(1, 10).Select(i => new TestOrderItem + { + Id = i, + ProductName = $"Product-{i}", + // All have SAME IId.Id = 1, but DIFFERENT instances + Assignee = new SharedUser { Id = 1, Username = "shared_user_name", Email = "shared@test.com" } + }).ToList() + }; + + // Arrange: 10 different instances with DIFFERENT IIds + var orderWithDifferentIIds = new TestOrder + { + Id = 1, + OrderNumber = "DIFF-IID", + Items = Enumerable.Range(1, 10).Select(i => new TestOrderItem + { + Id = i, + ProductName = $"Product-{i}", + // All have DIFFERENT IId.Id + Assignee = new SharedUser { Id = i * 100, Username = "unique_user_name", Email = "unique@test.com" } + }).ToList() + }; + + // Act + var sameIIdBinary = orderWithSameIId.ToBinary(); + var diffIIdBinary = orderWithDifferentIIds.ToBinary(); + var sameIIdResult = sameIIdBinary.BinaryTo(); + var diffIIdResult = diffIIdBinary.BinaryTo(); + + // Assert 1: Size comparison + Console.WriteLine($"Same IId binary size: {sameIIdBinary.Length} bytes"); + Console.WriteLine($"Different IId binary size: {diffIIdBinary.Length} bytes"); + Console.WriteLine($"Size difference: {diffIIdBinary.Length - sameIIdBinary.Length} bytes"); + + Assert.IsTrue(sameIIdBinary.Length < diffIIdBinary.Length, + $"Same IId ({sameIIdBinary.Length}b) should be smaller than different IIds ({diffIIdBinary.Length}b). " + + "IId-based deduplication NOT working!"); + + // Assert 2: Data integrity for sameIId result + Assert.IsNotNull(sameIIdResult, "sameIIdResult is null"); + Assert.IsNotNull(sameIIdResult.Items, "sameIIdResult.Items is null"); + Assert.AreEqual(10, sameIIdResult.Items.Count, "sameIIdResult should have 10 items"); + + for (var i = 0; i < 10; i++) + { + Assert.IsNotNull(sameIIdResult.Items[i].Assignee, + $"sameIIdResult.Items[{i}].Assignee is null - data lost!"); + Assert.AreEqual(1, sameIIdResult.Items[i].Assignee!.Id, + $"sameIIdResult.Items[{i}].Assignee.Id should be 1"); + Assert.AreEqual("shared_user_name", sameIIdResult.Items[i].Assignee.Username, + $"sameIIdResult.Items[{i}].Assignee.Username incorrect"); + } + + // Assert 3: Reference identity for sameIId + var firstAssignee = sameIIdResult.Items[0].Assignee; + for (var i = 1; i < 10; i++) + { + Assert.AreSame(firstAssignee, sameIIdResult.Items[i].Assignee, + $"CRITICAL: Items[{i}].Assignee should be same reference as Items[0].Assignee (same IId.Id=1)"); + } + } + + #endregion + + #region Guid-Based IId Tests + + /// + /// Guid IId with same instance - validates ObjectRef + data integrity + reference identity. + /// + [TestMethod] + public void GuidIId_SameInstance_SerializeAndDeserialize() + { + // Arrange: SAME instance + var sharedGuid = Guid.NewGuid(); + var sharedItem = new TestGuidItem { Id = sharedGuid, Name = "SharedGuidItem" }; + + var order = new TestGuidOrder + { + Id = Guid.NewGuid(), + Code = "GUID-001", + Count = 3, + Items = [sharedItem, sharedItem, sharedItem] + }; + + // Act + var binary = order.ToBinary(); + var result = binary.BinaryTo(); + + // Assert 1: ObjectRef should be present + var objectRefCount = CountObjectRefs(binary); + Console.WriteLine($"Binary size: {binary.Length} bytes"); + Console.WriteLine($"ObjectRef count: {objectRefCount}"); + + Assert.IsTrue(objectRefCount >= 2, $"Expected at least 2 ObjectRefs, found {objectRefCount}"); + + // Assert 2: Data integrity - all items present and correct + Assert.IsNotNull(result, "Result is null"); + Assert.IsNotNull(result.Items, "Items is null"); + Assert.AreEqual(3, result.Items.Count, "Items count incorrect"); + + for (var i = 0; i < 3; i++) + { + Assert.IsNotNull(result.Items[i], $"Items[{i}] is null"); + Assert.AreEqual(sharedGuid, result.Items[i].Id, $"Items[{i}].Id incorrect"); + Assert.AreEqual("SharedGuidItem", result.Items[i].Name, $"Items[{i}].Name incorrect"); + } + + // Assert 3: Reference identity + Assert.AreSame(result.Items[0], result.Items[1], "Items[0] and Items[1] should be same reference"); + Assert.AreSame(result.Items[1], result.Items[2], "Items[1] and Items[2] should be same reference"); + } + + /// + /// CRITICAL: Guid IId with different instances but same Id - tests IId-based deduplication. + /// + [TestMethod] + public void GuidIId_DifferentInstances_SameId_SerializeAndDeserialize() + { + // Arrange: DIFFERENT instances but SAME Guid + var sharedGuid = Guid.NewGuid(); + + var order = new TestGuidOrder + { + Id = Guid.NewGuid(), + Code = "GUID-001", + Count = 3, + Items = + [ + new TestGuidItem { Id = sharedGuid, Name = "SharedGuidItem" }, + new TestGuidItem { Id = sharedGuid, Name = "SharedGuidItem" }, + new TestGuidItem { Id = sharedGuid, Name = "SharedGuidItem" } + ] + }; + + // Act + var binary = order.ToBinary(); + var result = binary.BinaryTo(); + + // Assert 1: ObjectRef should be present if IId-based dedup works + var objectRefCount = CountObjectRefs(binary); + Console.WriteLine($"Binary size: {binary.Length} bytes"); + Console.WriteLine($"ObjectRef count: {objectRefCount}"); + + Assert.IsTrue(objectRefCount >= 2, + $"CRITICAL: Expected at least 2 ObjectRefs for same Guid IId, found {objectRefCount}. " + + "Guid-based IId deduplication NOT working!"); + + // Assert 2: Data integrity - all items present and correct + Assert.IsNotNull(result, "Result is null"); + Assert.IsNotNull(result.Items, "Items is null"); + Assert.AreEqual(3, result.Items.Count, "Items count incorrect"); + + for (var i = 0; i < 3; i++) + { + Assert.IsNotNull(result.Items[i], $"Items[{i}] is null - data lost!"); + Assert.AreEqual(sharedGuid, result.Items[i].Id, $"Items[{i}].Id incorrect"); + Assert.AreEqual("SharedGuidItem", result.Items[i].Name, $"Items[{i}].Name incorrect"); + } + + // Assert 3: Reference identity - if IId-based, should be same reference + Assert.AreSame(result.Items[0], result.Items[1], + "CRITICAL: Items[0] and Items[1] should be same reference (same Guid)"); + Assert.AreSame(result.Items[1], result.Items[2], + "CRITICAL: Items[1] and Items[2] should be same reference (same Guid)"); + } + + #endregion + + #region Data Integrity Tests + + /// + /// Verify data is correct regardless of reference handling. + /// + [TestMethod] + public void SharedCategory_DataIntegrity() + { + var categories = new List + { + new() { Id = 1, Name = "Category1", SortOrder = 1, IsDefault = true }, + new() { Id = 2, Name = "Category2", SortOrder = 2, ParentCategoryId = 1 }, + new() { Id = 3, Name = "Category3", SortOrder = 3, ParentCategoryId = 1 } + }; + + var binary = categories.ToBinary(); + var result = binary.BinaryTo>(); + + Assert.IsNotNull(result); + Assert.AreEqual(3, result.Count); + + Assert.AreEqual(1, result[0].Id); + Assert.AreEqual("Category1", result[0].Name); + Assert.IsTrue(result[0].IsDefault); + + Assert.AreEqual(2, result[1].Id); + Assert.AreEqual(1, result[1].ParentCategoryId); + + Assert.AreEqual(3, result[2].Id); + Assert.AreEqual(1, result[2].ParentCategoryId); + } + + #endregion +} diff --git a/AyCode.Core.Tests/Serialization/AcJsonSerializerIIdReferenceTests.cs b/AyCode.Core.Tests/Serialization/AcJsonSerializerIIdReferenceTests.cs new file mode 100644 index 0000000..b516cdd --- /dev/null +++ b/AyCode.Core.Tests/Serialization/AcJsonSerializerIIdReferenceTests.cs @@ -0,0 +1,441 @@ +using AyCode.Core.Extensions; +using AyCode.Core.Tests.TestModels; + +namespace AyCode.Core.Tests.Serialization; + +/// +/// Tests for IId-based reference handling in JSON serializer. +/// Two scenarios: +/// 1. Same instance referenced multiple times (object identity) +/// 2. Different instances with same IId.Id (IId-based deduplication) +/// +/// Tests verify BOTH: +/// - Serialized JSON uses $ref (not redundant full objects) +/// - Deserialized result maintains reference identity +/// +[TestClass] +public class AcJsonSerializerIIdReferenceTests +{ + #region Helper Methods + + /// + /// Counts occurrences of a string in JSON. + /// + private static int CountOccurrences(string json, string searchString) + { + var count = 0; + var index = 0; + while ((index = json.IndexOf(searchString, index, StringComparison.Ordinal)) != -1) + { + count++; + index += searchString.Length; + } + return count; + } + + #endregion + + #region Scenario 1: Same Instance (Object Identity) + + /// + /// SCENARIO 1: Same instance referenced multiple times. + /// JSON should contain $ref for 2nd, 3rd, 4th occurrence. + /// Validates: $ref present + data integrity after deserialize + reference identity + /// + [TestMethod] + public void SameInstance_Json_SerializeAndDeserialize() + { + // Arrange: SAME instance used 4 times + var sharedTag = new SharedTag { Id = 1, Name = "ImportantTag", Color = "#FF0000" }; + + var order = new TestOrder + { + Id = 1, + OrderNumber = "ORD-001", + PrimaryTag = sharedTag, + Items = + [ + new TestOrderItem { Id = 1, ProductName = "Product-A", Tag = sharedTag }, + new TestOrderItem { Id = 2, ProductName = "Product-B", Tag = sharedTag }, + new TestOrderItem { Id = 3, ProductName = "Product-C", Tag = sharedTag } + ] + }; + + // Act + var json = order.ToJson(); + Console.WriteLine(json); + var result = json.JsonTo(); + + // Assert 1: JSON contains $ref markers (reference handling is active) + var refCount = CountOccurrences(json, "{\"$ref\":\"1\"}"); + Console.WriteLine($"JSON length: {json.Length} chars"); + Console.WriteLine($"$ref count: {refCount}"); + + Assert.IsTrue(refCount >= 3, + $"Expected at least 3 $ref entries for shared tag, found {refCount}. " + + "Reference handling may not be active!"); + + // Assert 2: Data integrity - ALL tags are present and have correct data + Assert.IsNotNull(result, "Deserialized result is null"); + Assert.IsNotNull(result.PrimaryTag, "PrimaryTag is null - data lost!"); + Assert.AreEqual(1, result.PrimaryTag.Id, "PrimaryTag.Id incorrect"); + Assert.AreEqual("ImportantTag", result.PrimaryTag.Name, "PrimaryTag.Name incorrect"); + Assert.AreEqual("#FF0000", result.PrimaryTag.Color, "PrimaryTag.Color incorrect"); + + Assert.IsNotNull(result.Items, "Items is null"); + Assert.AreEqual(3, result.Items.Count, "Items count incorrect"); + + for (var i = 0; i < 3; i++) + { + Assert.IsNotNull(result.Items[i].Tag, $"Items[{i}].Tag is null - data lost!"); + Assert.AreEqual(1, result.Items[i].Tag!.Id, $"Items[{i}].Tag.Id incorrect"); + Assert.AreEqual("ImportantTag", result.Items[i].Tag.Name, $"Items[{i}].Tag.Name incorrect"); + } + + // Assert 3: Reference identity - all should be same object reference + Assert.AreSame(result.PrimaryTag, result.Items[0].Tag, + "Item[0].Tag should be same reference as PrimaryTag"); + Assert.AreSame(result.PrimaryTag, result.Items[1].Tag, + "Item[1].Tag should be same reference as PrimaryTag"); + Assert.AreSame(result.PrimaryTag, result.Items[2].Tag, + "Item[2].Tag should be same reference as PrimaryTag"); + } + + #endregion + + #region Scenario 2: Different Instances with Same IId (IId-Based Deduplication) + + /// + /// SCENARIO 2: DIFFERENT instances with SAME IId.Id value. + /// CRITICAL test - if IId-based deduplication works: + /// - $ref should be used in JSON + /// - Data should be complete after deserialize + /// - References should be identical (AreSame) + /// - Different TYPES with same int Id should NOT be confused! + /// + [TestMethod] + public void DifferentInstances_SameIId_SerializeAndDeserialize() + { + // Arrange: DIFFERENT instances but SAME IId.Id + // CRITICAL: Multiple DIFFERENT TYPES all have Id=1 - must not be confused! + var order = new TestOrder + { + Id = 1, + OrderNumber = "ORD-001", + // All three types have Id=1 - tests (Type, Id) keying, not just Id + PrimaryTag = new SharedTag { Id = 1, Name = "Tag_Id1", Color = "#FF0000" }, + Owner = new SharedUser { Id = 1, Username = "User_Id1", Email = "user1@test.com" }, + Category = new SharedCategory { Id = 1, Name = "Category_Id1", SortOrder = 10 }, + Items = + [ + new TestOrderItem + { + Id = 1, + ProductName = "Product-A", + Tag = new SharedTag { Id = 1, Name = "Tag_Id1", Color = "#FF0000" }, + Assignee = new SharedUser { Id = 1, Username = "User_Id1", Email = "user1@test.com" } + }, + new TestOrderItem + { + Id = 2, + ProductName = "Product-B", + Tag = new SharedTag { Id = 1, Name = "Tag_Id1", Color = "#FF0000" }, + Assignee = new SharedUser { Id = 1, Username = "User_Id1", Email = "user1@test.com" } + }, + new TestOrderItem + { + Id = 3, + ProductName = "Product-C", + Tag = new SharedTag { Id = 1, Name = "Tag_Id1", Color = "#FF0000" }, + Assignee = new SharedUser { Id = 1, Username = "User_Id1", Email = "user1@test.com" } + } + ] + }; + + // Act + var json = order.ToJson(); + var result = json.JsonTo(); + + // Assert 1: Check if $ref is used (IId-based deduplication active) + var refCount = CountOccurrences(json, "\"$ref\""); + Console.WriteLine($"JSON length: {json.Length} chars"); + Console.WriteLine($"$ref count: {refCount}"); + Console.WriteLine("JSON (first 2000 chars):"); + Console.WriteLine(json.Length > 2000 ? json[..2000] + "..." : json); + + // 4 Tags, 4 Users - each should have 3 $refs = 6 total minimum + Assert.IsTrue(refCount >= 6, + $"CRITICAL: Expected at least 6 $ref entries (3 per type for Tag and User), found {refCount}. " + + "IId-based reference deduplication is NOT working!"); + + // Assert 2: Data integrity - ALL data present and correct + Assert.IsNotNull(result, "Deserialized result is null"); + + // Tag data + Assert.IsNotNull(result.PrimaryTag, "PrimaryTag is null - data lost!"); + Assert.AreEqual(1, result.PrimaryTag.Id, "PrimaryTag.Id incorrect"); + Assert.AreEqual("Tag_Id1", result.PrimaryTag.Name, "PrimaryTag.Name incorrect - might be confused with User!"); + Assert.AreEqual("#FF0000", result.PrimaryTag.Color, "PrimaryTag.Color incorrect"); + + // User data - MUST NOT be confused with Tag (both have Id=1) + Assert.IsNotNull(result.Owner, "Owner is null - data lost!"); + Assert.AreEqual(1, result.Owner.Id, "Owner.Id incorrect"); + Assert.AreEqual("User_Id1", result.Owner.Username, "Owner.Username incorrect - might be confused with Tag!"); + Assert.AreEqual("user1@test.com", result.Owner.Email, "Owner.Email incorrect"); + + // Category data - MUST NOT be confused with Tag or User (all have Id=1) + Assert.IsNotNull(result.Category, "Category is null - data lost!"); + Assert.AreEqual(1, result.Category.Id, "Category.Id incorrect"); + Assert.AreEqual("Category_Id1", result.Category.Name, "Category.Name incorrect - might be confused with Tag!"); + Assert.AreEqual(10, result.Category.SortOrder, "Category.SortOrder incorrect"); + + Assert.IsNotNull(result.Items, "Items is null"); + Assert.AreEqual(3, result.Items.Count, "Items count incorrect"); + + for (var i = 0; i < 3; i++) + { + // Tag in items + Assert.IsNotNull(result.Items[i].Tag, $"Items[{i}].Tag is null - data lost!"); + Assert.AreEqual(1, result.Items[i].Tag!.Id, $"Items[{i}].Tag.Id incorrect"); + Assert.AreEqual("Tag_Id1", result.Items[i].Tag.Name, $"Items[{i}].Tag.Name incorrect - confused with User?"); + + // User in items - MUST NOT be confused with Tag + Assert.IsNotNull(result.Items[i].Assignee, $"Items[{i}].Assignee is null - data lost!"); + Assert.AreEqual(1, result.Items[i].Assignee!.Id, $"Items[{i}].Assignee.Id incorrect"); + Assert.AreEqual("User_Id1", result.Items[i].Assignee.Username, $"Items[{i}].Assignee.Username incorrect - confused with Tag?"); + } + + // Assert 3: Reference identity - same TYPE with same Id should be same reference + // Tags with Id=1 should all be same reference + Assert.AreSame(result.PrimaryTag, result.Items[0].Tag, + "CRITICAL: Item[0].Tag should be same reference as PrimaryTag (same SharedTag.Id=1)"); + Assert.AreSame(result.PrimaryTag, result.Items[1].Tag, + "CRITICAL: Item[1].Tag should be same reference as PrimaryTag (same SharedTag.Id=1)"); + Assert.AreSame(result.PrimaryTag, result.Items[2].Tag, + "CRITICAL: Item[2].Tag should be same reference as PrimaryTag (same SharedTag.Id=1)"); + + // Users with Id=1 should all be same reference + Assert.AreSame(result.Owner, result.Items[0].Assignee, + "CRITICAL: Item[0].Assignee should be same reference as Owner (same SharedUser.Id=1)"); + Assert.AreSame(result.Owner, result.Items[1].Assignee, + "CRITICAL: Item[1].Assignee should be same reference as Owner (same SharedUser.Id=1)"); + Assert.AreSame(result.Owner, result.Items[2].Assignee, + "CRITICAL: Item[2].Assignee should be same reference as Owner (same SharedUser.Id=1)"); + + // Assert 4: Different TYPES with same Id should NOT be same reference! + Assert.AreNotSame(result.PrimaryTag, result.Owner, + "CRITICAL BUG: Tag and User are same reference! Types with same int Id were confused!"); + Assert.AreNotSame(result.PrimaryTag, result.Category, + "CRITICAL BUG: Tag and Category are same reference! Types with same int Id were confused!"); + Assert.AreNotSame(result.Owner, result.Category, + "CRITICAL BUG: User and Category are same reference! Types with same int Id were confused!"); + } + + /// + /// Size comparison: Same IId should result in smaller JSON + verify data integrity. + /// + [TestMethod] + public void DifferentInstances_SameIId_SmallerJsonWithDataIntegrity() + { + // Arrange: 10 different instances with SAME IId + var orderWithSameIId = new TestOrder + { + Id = 1, + OrderNumber = "SAME-IID", + Items = Enumerable.Range(1, 10).Select(i => new TestOrderItem + { + Id = i, + ProductName = $"Product-{i}", + Assignee = new SharedUser { Id = 1, Username = "shared_user_name", Email = "shared@test.com" } + }).ToList() + }; + + // Arrange: 10 different instances with DIFFERENT IIds + var orderWithDifferentIIds = new TestOrder + { + Id = 1, + OrderNumber = "DIFF-IID", + Items = Enumerable.Range(1, 10).Select(i => new TestOrderItem + { + Id = i, + ProductName = $"Product-{i}", + Assignee = new SharedUser { Id = i * 100, Username = "unique_user_name", Email = "unique@test.com" } + }).ToList() + }; + + // Act + var sameIIdJson = orderWithSameIId.ToJson(); + var diffIIdJson = orderWithDifferentIIds.ToJson(); + var sameIIdResult = sameIIdJson.JsonTo(); + var diffIIdResult = diffIIdJson.JsonTo(); + + // Assert 1: Size comparison + Console.WriteLine($"Same IId JSON size: {sameIIdJson.Length} chars"); + Console.WriteLine($"Different IId JSON size: {diffIIdJson.Length} chars"); + Console.WriteLine($"Size difference: {diffIIdJson.Length - sameIIdJson.Length} chars"); + + Assert.IsTrue(sameIIdJson.Length < diffIIdJson.Length, + $"Same IId ({sameIIdJson.Length}) should be smaller than different IIds ({diffIIdJson.Length}). " + + "IId-based deduplication NOT working!"); + + // Assert 2: Data integrity for sameIId result + Assert.IsNotNull(sameIIdResult, "sameIIdResult is null"); + Assert.IsNotNull(sameIIdResult.Items, "sameIIdResult.Items is null"); + Assert.AreEqual(10, sameIIdResult.Items.Count, "sameIIdResult should have 10 items"); + + for (var i = 0; i < 10; i++) + { + Assert.IsNotNull(sameIIdResult.Items[i].Assignee, + $"sameIIdResult.Items[{i}].Assignee is null - data lost!"); + Assert.AreEqual(1, sameIIdResult.Items[i].Assignee!.Id, + $"sameIIdResult.Items[{i}].Assignee.Id should be 1"); + Assert.AreEqual("shared_user_name", sameIIdResult.Items[i].Assignee.Username, + $"sameIIdResult.Items[{i}].Assignee.Username incorrect"); + } + + // Assert 3: Reference identity for sameIId + var firstAssignee = sameIIdResult.Items[0].Assignee; + for (var i = 1; i < 10; i++) + { + Assert.AreSame(firstAssignee, sameIIdResult.Items[i].Assignee, + $"CRITICAL: Items[{i}].Assignee should be same reference as Items[0].Assignee (same IId.Id=1)"); + } + } + + #endregion + + #region Guid-Based IId Tests + + /// + /// Guid IId with same instance - validates $ref + data integrity + reference identity. + /// + [TestMethod] + public void GuidIId_SameInstance_SerializeAndDeserialize() + { + // Arrange: SAME instance + var sharedGuid = Guid.NewGuid(); + var sharedItem = new TestGuidItem { Id = sharedGuid, Name = "SharedGuidItem" }; + + var order = new TestGuidOrder + { + Id = Guid.NewGuid(), + Code = "GUID-001", + Count = 3, + Items = [sharedItem, sharedItem, sharedItem] + }; + + // Act + var json = order.ToJson(); + Console.WriteLine(json); + var result = json.JsonTo(); + + // Assert 1: $ref should be present + var refCount = CountOccurrences(json, "{\"$ref\":\"1\"}"); + Console.WriteLine($"JSON length: {json.Length} chars"); + Console.WriteLine($"$ref count: {refCount}"); + + Assert.IsTrue(refCount >= 2, $"Expected at least 2 $refs, found {refCount}"); + + // Assert 2: Data integrity - all items present and correct + Assert.IsNotNull(result, "Result is null"); + Assert.IsNotNull(result.Items, "Items is null"); + Assert.AreEqual(3, result.Items.Count, "Items count incorrect"); + + for (var i = 0; i < 3; i++) + { + Assert.IsNotNull(result.Items[i], $"Items[{i}] is null"); + Assert.AreEqual(sharedGuid, result.Items[i].Id, $"Items[{i}].Id incorrect"); + Assert.AreEqual("SharedGuidItem", result.Items[i].Name, $"Items[{i}].Name incorrect"); + } + + // Assert 3: Reference identity + Assert.AreSame(result.Items[0], result.Items[1], "Items[0] and Items[1] should be same reference"); + Assert.AreSame(result.Items[1], result.Items[2], "Items[1] and Items[2] should be same reference"); + } + + /// + /// CRITICAL: Guid IId with different instances but same Id - tests IId-based deduplication. + /// + [TestMethod] + public void GuidIId_DifferentInstances_SameId_SerializeAndDeserialize() + { + // Arrange: DIFFERENT instances but SAME Guid + var sharedGuid = Guid.NewGuid(); + + var order = new TestGuidOrder + { + Id = Guid.NewGuid(), + Code = "GUID-001", + Count = 3, + Items = + [ + new TestGuidItem { Id = sharedGuid, Name = "SharedGuidItem" }, + new TestGuidItem { Id = sharedGuid, Name = "SharedGuidItem" }, + new TestGuidItem { Id = sharedGuid, Name = "SharedGuidItem" } + ] + }; + + // Act + var json = order.ToJson(); + var result = json.JsonTo(); + + // Assert 1: $ref should be present if IId-based dedup works + var refCount = CountOccurrences(json, "\"$ref\""); + Console.WriteLine($"JSON length: {json.Length} chars"); + Console.WriteLine($"$ref count: {refCount}"); + Console.WriteLine("JSON:"); + Console.WriteLine(json); + + Assert.IsTrue(refCount >= 2, + $"CRITICAL: Expected at least 2 $refs for same Guid IId, found {refCount}. " + + "Guid-based IId deduplication NOT working!"); + + // Assert 2: Data integrity - all items present and correct + Assert.IsNotNull(result, "Result is null"); + Assert.IsNotNull(result.Items, "Items is null"); + Assert.AreEqual(3, result.Items.Count, "Items count incorrect"); + + for (var i = 0; i < 3; i++) + { + Assert.IsNotNull(result.Items[i], $"Items[{i}] is null - data lost!"); + Assert.AreEqual(sharedGuid, result.Items[i].Id, $"Items[{i}].Id incorrect"); + Assert.AreEqual("SharedGuidItem", result.Items[i].Name, $"Items[{i}].Name incorrect"); + } + + // Assert 3: Reference identity - if IId-based, should be same reference + Assert.AreSame(result.Items[0], result.Items[1], + "CRITICAL: Items[0] and Items[1] should be same reference (same Guid)"); + Assert.AreSame(result.Items[1], result.Items[2], + "CRITICAL: Items[1] and Items[2] should be same reference (same Guid)"); + } + + #endregion + + #region Data Integrity Tests + + [TestMethod] + public void SharedCategory_DataIntegrity() + { + var categories = new List + { + new() { Id = 1, Name = "Category1", SortOrder = 1, IsDefault = true }, + new() { Id = 2, Name = "Category2", SortOrder = 2, ParentCategoryId = 1 }, + new() { Id = 3, Name = "Category3", SortOrder = 3, ParentCategoryId = 1 } + }; + + var json = categories.ToJson(); + var result = json.JsonTo>(); + + Assert.IsNotNull(result); + Assert.AreEqual(3, result.Count); + + Assert.AreEqual(1, result[0].Id); + Assert.AreEqual("Category1", result[0].Name); + Assert.IsTrue(result[0].IsDefault); + + Assert.AreEqual(2, result[1].Id); + Assert.AreEqual(1, result[1].ParentCategoryId); + } + + #endregion +} diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.MetaWriter.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.MetaWriter.cs index 36fb25e..9fdad88 100644 --- a/AyCode.Core/Serializers/Toons/AcToonSerializer.MetaWriter.cs +++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.MetaWriter.cs @@ -9,15 +9,15 @@ public static partial class AcToonSerializer /// /// Write meta section only (for MetaOnly mode). /// - private static void WriteMetaSectionOnly(Type type, ToonSerializationContext context) + private static void WriteMetaSectionOnly(Type type, ToonSerializationContext context, string domainContext) { - WriteMetaSection(type, context); + WriteMetaSection(type, context, domainContext); } /// /// Write @meta and @types sections for a single root type. /// - private static void WriteMetaSection(Type type, ToonSerializationContext context) + private static void WriteMetaSection(Type type, ToonSerializationContext context, string domainContext) { if (!context.Options.UseMeta) return; @@ -25,13 +25,13 @@ public static partial class AcToonSerializer var typesToDocument = new HashSet(); CollectTypes(type, typesToDocument); - WriteMetaSectionCore(typesToDocument, context); + WriteMetaSectionCore(typesToDocument, context, domainContext); } /// /// Write @meta and @types sections for a collection of types. /// - private static void WriteMetaSection(IEnumerable types, ToonSerializationContext context) + private static void WriteMetaSection(IEnumerable types, ToonSerializationContext context, string domainContext) { if (!context.Options.UseMeta) return; @@ -42,13 +42,13 @@ public static partial class AcToonSerializer CollectTypes(type, typesToDocument); } - WriteMetaSectionCore(typesToDocument, context); + WriteMetaSectionCore(typesToDocument, context, domainContext); } /// /// Core logic for writing @meta and @types sections. /// - private static void WriteMetaSectionCore(HashSet typesToDocument, ToonSerializationContext context) + private static void WriteMetaSectionCore(HashSet typesToDocument, ToonSerializationContext context, string domainContext) { // @meta header context.WriteLine("@meta {"); @@ -57,6 +57,9 @@ public static partial class AcToonSerializer context.WriteProperty("format", "\"toon\""); context.WriteProperty("source-code-language", "\"C#\""); + if(!string.IsNullOrEmpty(domainContext)) + context.WriteProperty("context", $"\"{domainContext}\""); + // Write type list context.WriteIndent(); context.Write("types"); diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.cs index 2400e21..ef73fe0 100644 --- a/AyCode.Core/Serializers/Toons/AcToonSerializer.cs +++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.cs @@ -34,7 +34,8 @@ public static partial class AcToonSerializer /// /// Serialize object to Toon format with specified options. /// - public static string Serialize(T value, AcToonSerializerOptions options) + public static string Serialize(T value, AcToonSerializerOptions options) => Serialize(value, string.Empty, options); + public static string Serialize(T value, string domainDescription, AcToonSerializerOptions options) { if (value == null) return "null"; @@ -57,7 +58,7 @@ public static partial class AcToonSerializer switch (options.Mode) { case ToonSerializationMode.MetaOnly: - WriteMetaSectionOnly(type, context); + WriteMetaSectionOnly(type, context, domainDescription); break; case ToonSerializationMode.DataOnly: @@ -66,7 +67,7 @@ public static partial class AcToonSerializer case ToonSerializationMode.Full: default: - WriteMetaSection(type, context); + WriteMetaSection(type, context, domainDescription); context.WriteLine(); WriteDataSection(value, type, context); break; @@ -84,17 +85,19 @@ public static partial class AcToonSerializer /// Serialize only type metadata (schema) for a given type. /// Useful for sending type information once at conversation start. /// - public static string SerializeTypeMetadata() => SerializeTypeMetadata(typeof(T)); + public static string SerializeTypeMetadata() => SerializeTypeMetadata(typeof(T), string.Empty); + public static string SerializeTypeMetadata(string domainDescription) => SerializeTypeMetadata(typeof(T), domainDescription); /// /// Serialize only type metadata (schema) for a given type. /// - public static string SerializeTypeMetadata(Type type) + public static string SerializeTypeMetadata(Type type) => SerializeTypeMetadata(type, string.Empty); + public static string SerializeTypeMetadata(Type type, string domainDescription) { var context = ToonSerializationContextPool.Get(AcToonSerializerOptions.MetaOnly); try { - WriteMetaSectionOnly(type, context); + WriteMetaSectionOnly(type, context, domainDescription); return context.GetResult(); } finally @@ -110,7 +113,8 @@ public static partial class AcToonSerializer /// Types to document /// Serialization options (optional, defaults to MetaOnly preset) /// Metadata-only Toon format string with @meta and @types sections - public static string SerializeMetadata(IEnumerable types, AcToonSerializerOptions? options = null) + public static string SerializeMetadata(IEnumerable types, AcToonSerializerOptions? options = null) => SerializeMetadata(types, string.Empty, options); + public static string SerializeMetadata(IEnumerable types, string domainDescription, AcToonSerializerOptions? options = null) { // Return empty string if no types provided var typesList = types?.ToList(); @@ -124,7 +128,7 @@ public static partial class AcToonSerializer var context = ToonSerializationContextPool.Get(options); try { - WriteMetaSection(typesList, context); + WriteMetaSection(typesList, context, domainDescription); return context.GetResult(); } finally @@ -138,11 +142,8 @@ public static partial class AcToonSerializer /// /// Types to document /// Metadata-only Toon format string with @meta and @types sections - public static string SerializeMetadata(params Type[] types) - { - return SerializeMetadata((IEnumerable)types); - } - + public static string SerializeMetadata(params Type[] types) => SerializeMetadata((IEnumerable)types); + public static string SerializeMetadata(string domainDescription, params Type[] types) => SerializeMetadata((IEnumerable)types, domainDescription); #endregion #region Primitive Serialization