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