diff --git a/AllBenchmarksDropdown.html b/AllBenchmarksDropdown.html new file mode 100644 index 0000000..464fdac --- /dev/null +++ b/AllBenchmarksDropdown.html @@ -0,0 +1,82 @@ + + + + + BenchmarkDotNet Riportok (Dropdown) + + + +

BenchmarkDotNet Riportok

+ + +
+
+
+ + + diff --git a/AyCode.Core.Tests/Compression/GzipHelperTests.cs b/AyCode.Core.Tests/Compression/GzipHelperTests.cs new file mode 100644 index 0000000..0e63c89 --- /dev/null +++ b/AyCode.Core.Tests/Compression/GzipHelperTests.cs @@ -0,0 +1,52 @@ +using System.Buffers; +using System.Text; +using AyCode.Core.Compression; + +namespace AyCode.Core.Tests.Compression; + +[TestClass] +public class GzipHelperTests +{ + [TestMethod] + public void CompressAndDecompress_StringRoundTrip_Succeeds() + { + var original = "SignalR payload for gzip"; + + var compressed = GzipHelper.Compress(original); + var decompressed = GzipHelper.DecompressToString(compressed); + + Assert.IsNotNull(compressed); + Assert.AreNotEqual(0, compressed.Length); + Assert.AreEqual(original, decompressed); + } + + [TestMethod] + public void DecompressToRentedBuffer_ReturnsOriginalBytes() + { + var payload = "{\"message\":\"gzip\"}"; + var compressed = GzipHelper.Compress(payload); + + var (buffer, length) = GzipHelper.DecompressToRentedBuffer(compressed); + try + { + Assert.IsTrue(length > 0); + var text = Encoding.UTF8.GetString(buffer, 0, length); + Assert.AreEqual(payload, text); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + [TestMethod] + public void IsGzipCompressed_ReturnsExpectedValues() + { + var compressed = GzipHelper.Compress("ping"); + var nonCompressed = Encoding.UTF8.GetBytes("plain text"); + + Assert.IsTrue(GzipHelper.IsGzipCompressed(compressed)); + Assert.IsFalse(GzipHelper.IsGzipCompressed(nonCompressed)); + Assert.IsFalse(GzipHelper.IsGzipCompressed(Array.Empty())); + } +} diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerBasicTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerBasicTests.cs new file mode 100644 index 0000000..47dd827 --- /dev/null +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerBasicTests.cs @@ -0,0 +1,124 @@ +using AyCode.Core.Serializers.Binaries; + +namespace AyCode.Core.Tests.Serialization; + +/// +/// Basic serialization tests for primitive types. +/// +[TestClass] +public class AcBinarySerializerBasicTests +{ + [TestMethod] + public void Serialize_Null_ReturnsSingleNullByte() + { + var result = AcBinarySerializer.Serialize(null); + Assert.AreEqual(1, result.Length); + Assert.AreEqual((byte)0, result[0]); + } + + [TestMethod] + public void Serialize_Int32_RoundTrip() + { + var value = 12345; + var binary = AcBinarySerializer.Serialize(value); + var result = AcBinaryDeserializer.Deserialize(binary); + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Serialize_Int64_RoundTrip() + { + var value = 123456789012345L; + var binary = AcBinarySerializer.Serialize(value); + var result = AcBinaryDeserializer.Deserialize(binary); + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Serialize_Double_RoundTrip() + { + var value = 3.14159265358979; + var binary = AcBinarySerializer.Serialize(value); + var result = AcBinaryDeserializer.Deserialize(binary); + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Serialize_String_RoundTrip() + { + var value = "Hello, Binary World!"; + var binary = AcBinarySerializer.Serialize(value); + var result = AcBinaryDeserializer.Deserialize(binary); + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Serialize_Boolean_RoundTrip() + { + var trueResult = AcBinaryDeserializer.Deserialize(AcBinarySerializer.Serialize(true)); + var falseResult = AcBinaryDeserializer.Deserialize(AcBinarySerializer.Serialize(false)); + Assert.IsTrue(trueResult); + Assert.IsFalse(falseResult); + } + + [TestMethod] + public void Serialize_DateTime_RoundTrip() + { + var value = new DateTime(2024, 12, 25, 10, 30, 45, DateTimeKind.Utc); + var binary = AcBinarySerializer.Serialize(value); + var result = AcBinaryDeserializer.Deserialize(binary); + Assert.AreEqual(value, result); + } + + [TestMethod] + [DataRow(DateTimeKind.Unspecified)] + [DataRow(DateTimeKind.Utc)] + [DataRow(DateTimeKind.Local)] + public void Serialize_DateTime_PreservesKind(DateTimeKind kind) + { + var value = new DateTime(2024, 12, 25, 10, 30, 45, kind); + var binary = AcBinarySerializer.Serialize(value); + var result = AcBinaryDeserializer.Deserialize(binary); + + Assert.AreEqual(value.Ticks, result.Ticks); + Assert.AreEqual(value.Kind, result.Kind); + } + + [TestMethod] + public void Serialize_Guid_RoundTrip() + { + var value = Guid.NewGuid(); + var binary = AcBinarySerializer.Serialize(value); + var result = AcBinaryDeserializer.Deserialize(binary); + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Serialize_Decimal_RoundTrip() + { + var value = 123456.789012m; + var binary = AcBinarySerializer.Serialize(value); + var result = AcBinaryDeserializer.Deserialize(binary); + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Serialize_TimeSpan_RoundTrip() + { + var value = TimeSpan.FromHours(2.5); + var binary = AcBinarySerializer.Serialize(value); + var result = AcBinaryDeserializer.Deserialize(binary); + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Serialize_DateTimeOffset_RoundTrip() + { + var value = new DateTimeOffset(2024, 12, 25, 10, 30, 45, TimeSpan.FromHours(2)); + var binary = AcBinarySerializer.Serialize(value); + var result = AcBinaryDeserializer.Deserialize(binary); + + Assert.AreEqual(value.UtcTicks, result.UtcTicks); + Assert.AreEqual(value.Offset, result.Offset); + } +} diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerBenchmarkTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerBenchmarkTests.cs new file mode 100644 index 0000000..7c0f194 --- /dev/null +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerBenchmarkTests.cs @@ -0,0 +1,82 @@ +using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Tests.TestModels; + +namespace AyCode.Core.Tests.Serialization; + +[TestClass] +public class AcBinarySerializerBenchmarkTests +{ + [TestMethod] + public void Serialize_BenchmarkOrder_RoundTrip() + { + var order = TestDataFactory.CreateBenchmarkOrder(itemCount: 3, palletsPerItem: 2, measurementsPerPallet: 2, pointsPerMeasurement: 5); + + var binary = AcBinarySerializer.Serialize(order); + Assert.IsTrue(binary.Length > 0, "Binary data should not be empty"); + + var result = AcBinaryDeserializer.Deserialize(binary); + Assert.IsNotNull(result); + Assert.AreEqual(order.Id, result.Id); + Assert.AreEqual(order.OrderNumber, result.OrderNumber); + Assert.AreEqual(order.Items.Count, result.Items.Count); + } + + [TestMethod] + public void Serialize_BenchmarkOrder_SmallData_RoundTrip() + { + var order = TestDataFactory.CreateBenchmarkOrder(itemCount: 1, palletsPerItem: 1, measurementsPerPallet: 1, pointsPerMeasurement: 1); + + var binary = AcBinarySerializer.Serialize(order); + Assert.IsTrue(binary.Length > 0); + + var result = AcBinaryDeserializer.Deserialize(binary); + Assert.IsNotNull(result); + Assert.AreEqual(order.Id, result.Id); + } + + [TestMethod] + public void Serialize_BenchmarkOrder_LargeData_RoundTrip() + { + var order = TestDataFactory.CreateBenchmarkOrder(itemCount: 10, palletsPerItem: 5, measurementsPerPallet: 3, pointsPerMeasurement: 10); + + var binary = AcBinarySerializer.Serialize(order); + Assert.IsTrue(binary.Length > 0, "Binary data should not be empty"); + + var result = AcBinaryDeserializer.Deserialize(binary); + Assert.IsNotNull(result); + Assert.AreEqual(order.Id, result.Id); + Assert.AreEqual(order.OrderNumber, result.OrderNumber); + Assert.AreEqual(order.Items.Count, result.Items.Count); + + // Verify nested structure + for (int i = 0; i < order.Items.Count; i++) + { + Assert.AreEqual(order.Items[i].Id, result.Items[i].Id); + Assert.AreEqual(order.Items[i].Pallets.Count, result.Items[i].Pallets.Count); + } + } + + [TestMethod] + public void Serialize_BenchmarkOrder_WithStringInterning_SmallerThanWithout() + { + var order = TestDataFactory.CreateBenchmarkOrder(itemCount: 5, palletsPerItem: 3, measurementsPerPallet: 2, pointsPerMeasurement: 5); + + var binaryWithInterning = AcBinarySerializer.Serialize(order, AcBinarySerializerOptions.Default); + var binaryWithoutInterning = AcBinarySerializer.Serialize(order, new AcBinarySerializerOptions { UseStringInterning = false }); + + // Note: String interning may not always result in smaller size due to header overhead + // The primary benefit is for larger datasets with many repeated strings + Console.WriteLine($"With interning: {binaryWithInterning.Length}, Without: {binaryWithoutInterning.Length}"); + + // Both should deserialize correctly regardless of size + var result1 = AcBinaryDeserializer.Deserialize(binaryWithInterning); + var result2 = AcBinaryDeserializer.Deserialize(binaryWithoutInterning); + + Assert.IsNotNull(result1); + Assert.IsNotNull(result2); + Assert.AreEqual(order.Id, result1.Id); + Assert.AreEqual(order.Id, result2.Id); + Assert.AreEqual(order.Items.Count, result1.Items.Count); + Assert.AreEqual(order.Items.Count, result2.Items.Count); + } +} diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerCircularReferenceTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerCircularReferenceTests.cs new file mode 100644 index 0000000..c58bc74 --- /dev/null +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerCircularReferenceTests.cs @@ -0,0 +1,160 @@ +using AyCode.Core.Extensions; +using static AyCode.Core.Tests.Serialization.AcSerializerModels; + +namespace AyCode.Core.Tests.Serialization; + +/// +/// Tests for circular reference handling with back-navigation properties. +/// +[TestClass] +public class AcBinarySerializerCircularReferenceTests +{ + /// + /// CRITICAL TEST: Circular references with back-navigation properties. + /// This simulates the exact production scenario where: + /// - StockTaking has StockTakingItems collection + /// - StockTakingItem has StockTaking back-reference (circular!) + /// - StockTakingItem has Product navigation property + /// + [TestMethod] + public void Deserialize_CircularReference_ParentChildBackReference() + { + var parent = new CircularParent + { + Id = 1, + Name = "Parent", + Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc), + Modified = DateTime.UtcNow, + Creator = 6, + Children = new List() + }; + + var child = new CircularChild + { + Id = 10, + ParentId = 1, + Name = "Child", + Created = DateTime.UtcNow.AddHours(-1), + Modified = DateTime.UtcNow, + Parent = parent, + GrandChildren = new List() + }; + + var grandChild = new CircularGrandChild + { + Id = 100, + ChildId = 10, + CreatorId = 6, + ModifierId = null, + Created = DateTime.UtcNow, + Modified = DateTime.UtcNow, + Child = child + }; + + child.GrandChildren.Add(grandChild); + parent.Children.Add(child); + + var binary = parent.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(1, result.Id); + Assert.AreEqual(6, result.Creator, "Creator should be 6"); + Assert.AreEqual(parent.Created.Ticks, result.Created.Ticks, + $"Created mismatch. Expected: {parent.Created}, Got: {result.Created}"); + + Assert.IsNotNull(result.Children); + Assert.AreEqual(1, result.Children.Count); + + var resultChild = result.Children[0]; + Assert.AreEqual(10, resultChild.Id); + Assert.AreEqual(resultChild.Created.Ticks, child.Created.Ticks, "Child.Created should match"); + + Assert.IsNotNull(resultChild.Parent, "Child.Parent back-reference should be resolved"); + Assert.AreEqual(1, resultChild.Parent.Id, "Back-reference should point to same parent"); + } + + /// + /// Test list of parents with circular references. + /// + [TestMethod] + public void Deserialize_ListOfCircularReferences_AllItemsCorrect() + { + var parents = Enumerable.Range(1, 5).Select(p => + { + var parent = new CircularParent + { + Id = p, + Name = $"Parent_{p}", + Created = DateTime.UtcNow.AddDays(-p), + Modified = DateTime.UtcNow, + Creator = p, + Children = new List() + }; + + for (int c = 1; c <= 2; c++) + { + var child = new CircularChild + { + Id = p * 100 + c, + ParentId = p, + Name = $"Child_{p}_{c}", + Created = DateTime.UtcNow.AddHours(-c), + Modified = DateTime.UtcNow, + Parent = parent, + GrandChildren = new List() + }; + + for (int g = 1; g <= 2; g++) + { + child.GrandChildren.Add(new CircularGrandChild + { + Id = p * 1000 + c * 100 + g, + ChildId = child.Id, + CreatorId = g % 2 == 0 ? p : null, + ModifierId = g % 2 == 1 ? p * 2 : null, + Created = DateTime.UtcNow, + Modified = DateTime.UtcNow, + Child = child + }); + } + + parent.Children.Add(child); + } + + return parent; + }).ToList(); + + var binary = parents.ToBinary(); + var result = binary.BinaryTo>(); + + Assert.IsNotNull(result); + Assert.AreEqual(5, result.Count); + + for (int p = 0; p < 5; p++) + { + var original = parents[p]; + var deserialized = result[p]; + + Assert.AreEqual(original.Id, deserialized.Id, $"Parent[{p}].Id mismatch"); + Assert.AreEqual(original.Creator, deserialized.Creator, $"Parent[{p}].Creator mismatch"); + Assert.AreEqual(original.Created.Ticks, deserialized.Created.Ticks, + $"Parent[{p}].Created mismatch. Expected: {original.Created}, Got: {deserialized.Created}"); + + Assert.IsNotNull(deserialized.Children, $"Parent[{p}].Children is null"); + Assert.AreEqual(2, deserialized.Children.Count, $"Parent[{p}].Children.Count mismatch"); + + for (int c = 0; c < 2; c++) + { + var origChild = original.Children![c]; + var deserChild = deserialized.Children[c]; + + Assert.AreEqual(origChild.Id, deserChild.Id, $"Parent[{p}].Children[{c}].Id mismatch"); + Assert.AreEqual(origChild.Created.Ticks, deserChild.Created.Ticks, + $"Parent[{p}].Children[{c}].Created mismatch"); + + Assert.IsNotNull(deserChild.Parent, $"Parent[{p}].Children[{c}].Parent should not be null"); + } + } + } +} diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerDateTimeTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerDateTimeTests.cs new file mode 100644 index 0000000..8cc848c --- /dev/null +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerDateTimeTests.cs @@ -0,0 +1,188 @@ +using AyCode.Core.Extensions; +using static AyCode.Core.Tests.Serialization.AcSerializerModels; +using static AyCode.Core.Tests.Serialization.AcSerializerTestHelper; + +namespace AyCode.Core.Tests.Serialization; + +/// +/// Tests for DateTime type handling and potential type mismatch issues. +/// +[TestClass] +public class AcBinarySerializerDateTimeTests +{ + [TestMethod] + public void Deserialize_DateTimeProperty_FromDifferentPropertyOrder_RoundTrip() + { + var entity = new TestEntityWithDateTimeAndInt + { + Id = 42, + IntValue = 100, + Created = new DateTime(2024, 12, 25, 10, 30, 45, DateTimeKind.Utc), + Modified = new DateTime(2024, 12, 26, 11, 45, 30, DateTimeKind.Utc), + StatusCode = 5, + Name = "TestEntity" + }; + + var binary = entity.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(42, result.Id); + Assert.AreEqual(100, result.IntValue); + Assert.AreEqual(entity.Created, result.Created, "Created DateTime should match"); + Assert.AreEqual(entity.Modified, result.Modified, "Modified DateTime should match"); + Assert.AreEqual(5, result.StatusCode); + Assert.AreEqual("TestEntity", result.Name); + } + + [TestMethod] + public void Deserialize_ListOfEntitiesWithDateTimeProperties_RoundTrip() + { + var entities = CreateDateTimeEntities(10); + var binary = entities.ToBinary(); + var result = binary.BinaryTo>(); + + Assert.IsNotNull(result); + Assert.AreEqual(10, result.Count); + + for (int i = 0; i < 10; i++) + { + var original = entities[i]; + var deserialized = result[i]; + + Assert.AreEqual(original.Id, deserialized.Id, $"Id mismatch at index {i}"); + Assert.AreEqual(original.IntValue, deserialized.IntValue, $"IntValue mismatch at index {i}"); + Assert.AreEqual(original.Created.Ticks, deserialized.Created.Ticks, $"Created mismatch at index {i}"); + Assert.AreEqual(original.Modified.Ticks, deserialized.Modified.Ticks, $"Modified mismatch at index {i}"); + Assert.AreEqual(original.StatusCode, deserialized.StatusCode, $"StatusCode mismatch at index {i}"); + Assert.AreEqual(original.Name, deserialized.Name, $"Name mismatch at index {i}"); + } + } + + [TestMethod] + public void Deserialize_EntityWithManyIntPropertiesBeforeDateTime_RoundTrip() + { + var entity = new TestEntityWithManyIntsBeforeDateTime + { + Id = 1, + Value1 = 10, + Value2 = 20, + Value3 = 30, + Value4 = 40, + Value5 = 50, + FirstDateTime = new DateTime(2024, 1, 15, 10, 0, 0, DateTimeKind.Utc), + SecondDateTime = new DateTime(2024, 6, 20, 15, 30, 0, DateTimeKind.Utc), + FinalValue = 999 + }; + + var binary = entity.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(1, result.Id); + Assert.AreEqual(10, result.Value1); + Assert.AreEqual(20, result.Value2); + Assert.AreEqual(30, result.Value3); + Assert.AreEqual(40, result.Value4); + Assert.AreEqual(50, result.Value5); + Assert.AreEqual(entity.FirstDateTime, result.FirstDateTime, "FirstDateTime should match"); + Assert.AreEqual(entity.SecondDateTime, result.SecondDateTime, "SecondDateTime should match"); + Assert.AreEqual(999, result.FinalValue); + } + + [TestMethod] + public void Deserialize_NestedEntityWithDateTimeInChild_RoundTrip() + { + var parent = new TestParentEntityWithDateTimeChild + { + ParentId = 1, + ParentName = "Parent", + Child = new TestEntityWithDateTimeAndInt + { + Id = 100, + IntValue = 200, + Created = DateTime.UtcNow.AddDays(-5), + Modified = DateTime.UtcNow, + StatusCode = 3, + Name = "Child" + } + }; + + var binary = parent.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(1, result.ParentId); + Assert.AreEqual("Parent", result.ParentName); + Assert.IsNotNull(result.Child); + Assert.AreEqual(100, result.Child.Id); + Assert.AreEqual(200, result.Child.IntValue); + Assert.AreEqual(parent.Child.Created.Ticks, result.Child.Created.Ticks, "Child.Created should match"); + Assert.AreEqual(parent.Child.Modified.Ticks, result.Child.Modified.Ticks, "Child.Modified should match"); + } + + [TestMethod] + public void Deserialize_EntityWithCollectionContainingDateTimeItems_RoundTrip() + { + var parent = new TestParentWithDateTimeItemCollection + { + Id = 1, + Name = "Parent", + Created = DateTime.UtcNow.AddDays(-10), + Items = CreateDateTimeEntities(5) + }; + + var binary = parent.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(1, result.Id); + Assert.AreEqual("Parent", result.Name); + Assert.AreEqual(parent.Created.Ticks, result.Created.Ticks, "Parent.Created should match"); + Assert.IsNotNull(result.Items); + Assert.AreEqual(5, result.Items.Count); + + for (int i = 0; i < 5; i++) + { + var originalItem = parent.Items[i]; + var deserializedItem = result.Items[i]; + + Assert.AreEqual(originalItem.Id, deserializedItem.Id, $"Items[{i}].Id should match"); + Assert.AreEqual(originalItem.IntValue, deserializedItem.IntValue, $"Items[{i}].IntValue should match"); + Assert.AreEqual(originalItem.Created.Ticks, deserializedItem.Created.Ticks, $"Items[{i}].Created should match"); + Assert.AreEqual(originalItem.Modified.Ticks, deserializedItem.Modified.Ticks, $"Items[{i}].Modified should match"); + } + } + + [TestMethod] + public void Deserialize_ListOfParentEntitiesWithDateTimeChildCollections_RoundTrip() + { + var parents = CreateParentWithDateTimeItems(3, 3); + var binary = parents.ToBinary(); + var result = binary.BinaryTo>(); + + Assert.IsNotNull(result); + Assert.AreEqual(3, result.Count); + + for (int p = 0; p < 3; p++) + { + var originalParent = parents[p]; + var deserializedParent = result[p]; + + Assert.AreEqual(originalParent.Id, deserializedParent.Id, $"Parent[{p}].Id should match"); + Assert.AreEqual(originalParent.Name, deserializedParent.Name, $"Parent[{p}].Name should match"); + Assert.AreEqual(originalParent.Created.Ticks, deserializedParent.Created.Ticks, $"Parent[{p}].Created should match"); + Assert.IsNotNull(deserializedParent.Items); + Assert.AreEqual(3, deserializedParent.Items.Count, $"Parent[{p}].Items.Count should match"); + + for (int i = 0; i < 3; i++) + { + var originalItem = originalParent.Items![i]; + var deserializedItem = deserializedParent.Items[i]; + + Assert.AreEqual(originalItem.Id, deserializedItem.Id, $"Parent[{p}].Items[{i}].Id should match"); + Assert.AreEqual(originalItem.Created.Ticks, deserializedItem.Created.Ticks, $"Parent[{p}].Items[{i}].Created should match. Expected: {originalItem.Created}, Got: {deserializedItem.Created}"); + } + } + } +} diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerDiagnosticTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerDiagnosticTests.cs new file mode 100644 index 0000000..dd53541 --- /dev/null +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerDiagnosticTests.cs @@ -0,0 +1,474 @@ +using AyCode.Core.Extensions; +using AyCode.Core.Serializers.Binaries; +using System.Reflection; +using static AyCode.Core.Tests.Serialization.AcSerializerModels; + +namespace AyCode.Core.Tests.Serialization; + +/// +/// Diagnostic tests to help debug serialization issues. +/// +[TestClass] +public class AcBinarySerializerDiagnosticTests +{ + /// + /// Diagnostic test to understand the exact binary structure. + /// This test outputs the binary bytes to help debug production issues. + /// + [TestMethod] + public void Diagnostic_StockTaking_BinaryStructure() + { + var stockTaking = new TestStockTakingWithInheritance + { + Id = 1, + StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc), + IsClosed = false, + Creator = 6, + Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc), + Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc), + StockTakingItems = null + }; + + var binary = stockTaking.ToBinary(); + + var hexDump = string.Join(" ", binary.Select(b => b.ToString("X2"))); + Console.WriteLine($"Binary length: {binary.Length}"); + Console.WriteLine($"Binary hex: {hexDump}"); + + for (int i = 0; i < binary.Length; i++) + { + if (binary[i] == 214) + { + Console.WriteLine($"Found 0xD6 at position {i}"); + } + } + + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(6, result.Creator); + Assert.AreEqual(stockTaking.Created.Ticks, result.Created.Ticks); + } + + /// + /// Test with nested list to ensure proper stream positioning. + /// + [TestMethod] + public void Diagnostic_StockTaking_WithNestedItems() + { + var stockTaking = new TestStockTakingWithInheritance + { + Id = 1, + StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc), + IsClosed = false, + Creator = 6, + Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc), + Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc), + StockTakingItems = new List + { + new() + { + Id = 10, + StockTakingId = 1, + ProductId = 100, + IsMeasured = true, + OriginalStockQuantity = 50, + MeasuredStockQuantity = 48, + Created = new DateTime(2025, 1, 24, 14, 0, 0, DateTimeKind.Utc), + Modified = new DateTime(2025, 1, 24, 14, 30, 0, DateTimeKind.Utc), + StockTakingItemPallets = null + } + } + }; + + var binary = stockTaking.ToBinary(); + Console.WriteLine($"Binary length with 1 item: {binary.Length}"); + + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(6, result.Creator, "Creator should be 6"); + Assert.AreEqual(stockTaking.Created.Ticks, result.Created.Ticks, + $"Created mismatch. Expected: {stockTaking.Created}, Got: {result.Created}"); + Assert.IsNotNull(result.StockTakingItems); + Assert.AreEqual(1, result.StockTakingItems.Count); + } + + /// + /// CRITICAL TEST: Verify property order is consistent. + /// This test checks that the reflection-based property order matches + /// what's expected for serialization/deserialization. + /// + [TestMethod] + public void Diagnostic_PropertyOrder_InheritanceHierarchy() + { + var type = typeof(SimStockTaking); + var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + + Console.WriteLine($"Properties of {type.Name} (count: {props.Length}):"); + for (int i = 0; i < props.Length; i++) + { + var prop = props[i]; + Console.WriteLine($" [{i}] {prop.Name} : {prop.PropertyType.Name} (declared in: {prop.DeclaringType?.Name})"); + } + + // The exact order may vary by platform! + // Log it so we can compare server vs client + Assert.IsTrue(props.Length >= 7, "Should have at least 7 properties"); + + // Check that all expected properties exist + var propNames = props.Select(p => p.Name).ToHashSet(); + Assert.IsTrue(propNames.Contains("Id"), "Should have Id"); + Assert.IsTrue(propNames.Contains("StartDateTime"), "Should have StartDateTime"); + Assert.IsTrue(propNames.Contains("IsClosed"), "Should have IsClosed"); + Assert.IsTrue(propNames.Contains("Creator"), "Should have Creator"); + Assert.IsTrue(propNames.Contains("Created"), "Should have Created"); + Assert.IsTrue(propNames.Contains("Modified"), "Should have Modified"); + Assert.IsTrue(propNames.Contains("StockTakingItems"), "Should have StockTakingItems"); + } + + /// + /// CRITICAL REGRESSION TEST: Simulates exact production hierarchy. + /// StockTaking : MgStockTaking<StockTakingItem> : MgEntityBase : BaseEntity + /// + [TestMethod] + public void Diagnostic_SimStockTaking_RoundTrip() + { + var stockTaking = new SimStockTaking + { + Id = 1, + StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc), + IsClosed = false, + Creator = 6, // The exact value from production error + Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc), + Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc), + StockTakingItems = null // loadRelations = false means no items + }; + + var binary = stockTaking.ToBinary(); + + // Log the property names in the header + Console.WriteLine($"Binary length: {binary.Length}"); + + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(1, result.Id, "Id should be 1"); + Assert.AreEqual(6, result.Creator, "Creator should be 6 - this is where the bug occurs!"); + Assert.AreEqual(stockTaking.Created.Ticks, result.Created.Ticks, + $"Created mismatch. Expected: {stockTaking.Created}, Got: {result.Created}"); + Assert.AreEqual(stockTaking.StartDateTime, result.StartDateTime); + Assert.IsFalse(result.IsClosed); + } + + /// + /// Test List of SimStockTaking - exact production scenario. + /// + [TestMethod] + public void Diagnostic_ListOfSimStockTaking_RoundTrip() + { + var stockTakings = Enumerable.Range(1, 3).Select(i => new SimStockTaking + { + Id = i, + StartDateTime = DateTime.UtcNow.AddDays(-i), + IsClosed = i % 2 == 0, + Creator = i, + Created = DateTime.UtcNow.AddDays(-i), + Modified = DateTime.UtcNow, + StockTakingItems = null + }).ToList(); + + var binary = stockTakings.ToBinary(); + var result = binary.BinaryTo>(); + + Assert.IsNotNull(result); + Assert.AreEqual(3, result.Count); + + for (int i = 0; i < 3; i++) + { + var original = stockTakings[i]; + var deserialized = result[i]; + + Assert.AreEqual(original.Id, deserialized.Id, $"[{i}] Id mismatch"); + Assert.AreEqual(original.Creator, deserialized.Creator, $"[{i}] Creator mismatch"); + Assert.AreEqual(original.Created.Ticks, deserialized.Created.Ticks, + $"[{i}] Created mismatch. Expected: {original.Created}, Got: {deserialized.Created}"); + } + } + + /// + /// Diagnostic: Check what PropertyType the reflection returns for generic type parameter. + /// + [TestMethod] + public void Diagnostic_GenericProperty_ReflectionType() + { + var parentType = typeof(ConcreteParent); + var itemsProp = parentType.GetProperty("Items"); + + Assert.IsNotNull(itemsProp); + + var propType = itemsProp.PropertyType; + Console.WriteLine($"PropertyType: {propType}"); + Console.WriteLine($"PropertyType.FullName: {propType.FullName}"); + Console.WriteLine($"IsGenericType: {propType.IsGenericType}"); + + if (propType.IsGenericType) + { + var args = propType.GetGenericArguments(); + Console.WriteLine($"GenericArguments.Length: {args.Length}"); + foreach (var arg in args) + { + Console.WriteLine($" GenericArgument: {arg.FullName}"); + } + } + + Assert.IsTrue(propType.IsGenericType); + var elementType = propType.GetGenericArguments()[0]; + Assert.AreEqual(typeof(GenericItemImpl), elementType, + "Element type should be GenericItemImpl, not IGenericItem"); + } + + /// + /// CRITICAL BUG REPRODUCTION: StockTakingItems is null (loadRelations = false) + /// This test verifies what happens when: + /// 1. Metadata header registers ALL properties including StockTakingItems + /// 2. Body SKIPS StockTakingItems because it's null + /// 3. Deserializer reads the body and must correctly map indices + /// + [TestMethod] + public void Diagnostic_NullStockTakingItems_VerifyPropertyIndices() + { + var stockTaking = new SimStockTaking + { + Id = 1, + StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc), + IsClosed = false, + Creator = 6, // The exact value from production error (becomes TinyInt 0xD6) + Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc), + Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc), + StockTakingItems = null // THIS IS THE KEY - loadRelations = false + }; + + var binary = stockTaking.ToBinary(); + + // Log the binary structure + Console.WriteLine($"Binary length: {binary.Length}"); + + // Parse the header manually to understand structure + var pos = 0; + var version = binary[pos++]; + Console.WriteLine($"Version: {version}"); + + var marker = binary[pos++]; + Console.WriteLine($"Marker: 0x{marker:X2}"); + + // Read property count from metadata header + if ((marker & 0x10) != 0) // HasMetadata flag + { + var propCount = binary[pos++]; + Console.WriteLine($"\n=== METADATA HEADER ==="); + Console.WriteLine($"Property count in header: {propCount}"); + + for (int i = 0; i < propCount; i++) + { + var strLen = binary[pos++]; + var propName = System.Text.Encoding.UTF8.GetString(binary, pos, strLen); + pos += strLen; + Console.WriteLine($" Header property [{i}]: '{propName}'"); + } + } + + Console.WriteLine($"\n=== BODY (starts at position {pos}) ==="); + + // The body should start with Object marker (0x19) + var bodyStart = pos; + var objectMarker = binary[pos++]; + Console.WriteLine($"Object marker: 0x{objectMarker:X2} (expected 0x19 for Object)"); + + // Read ref ID (if reference handling is enabled) + // VarInt: if top bit is set, continue reading + var refIdByte = binary[pos]; + int refId; + if ((refIdByte & 0x80) == 0) + { + refId = refIdByte; + pos++; + } + else + { + // Multi-byte VarInt - simplified parsing + refId = -1; + pos += 2; // Skip for now + } + Console.WriteLine($"RefId: {refId}"); + + // Read property count in body + var bodyPropCount = binary[pos++]; + Console.WriteLine($"Property count in body: {bodyPropCount}"); + + Console.WriteLine($"\n=== BODY PROPERTIES ==="); + for (int i = 0; i < bodyPropCount && pos < binary.Length; i++) + { + var propIndex = binary[pos++]; + Console.WriteLine($" Body property [{i}]: index={propIndex}, next bytes: 0x{binary[pos]:X2} 0x{(pos + 1 < binary.Length ? binary[pos + 1] : 0):X2}"); + + // Skip the value (simplified - just log) + var valueType = binary[pos]; + if (valueType == 0x14) // DateTime + { + Console.WriteLine($" -> DateTime (9 bytes)"); + pos += 10; // type + 9 bytes + } + else if (valueType >= 0xD0 && valueType <= 0xE7) // TinyInt + { + var tinyValue = valueType - 0xD0; + Console.WriteLine($" -> TinyInt value: {tinyValue}"); + pos += 1; + } + else if (valueType == 0x03) // False + { + Console.WriteLine($" -> Boolean: false"); + pos += 1; + } + else if (valueType == 0x02) // True + { + Console.WriteLine($" -> Boolean: true"); + pos += 1; + } + else + { + Console.WriteLine($" -> Unknown type: 0x{valueType:X2}"); + break; + } + } + + // Find where 0xD6 (Creator = 6) appears in the body + Console.WriteLine($"\n=== 0xD6 OCCURRENCES ==="); + for (int i = bodyStart; i < binary.Length; i++) + { + if (binary[i] == 0xD6) + { + Console.WriteLine($"Found 0xD6 (TinyInt 6 = Creator value) at position {i}"); + } + } + + // Deserialize and verify + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(1, result.Id, "Id should be 1"); + Assert.AreEqual(6, result.Creator, + $"Creator should be 6. Got: {result.Creator}. " + + $"If this fails with a very large number, it means DateTime bytes were interpreted as int!"); + Assert.AreEqual(stockTaking.Created, result.Created, + $"Created mismatch. Expected: {stockTaking.Created}, Got: {result.Created}. " + + $"If Created has wrong value, deserializer read wrong bytes!"); + Assert.AreEqual(stockTaking.StartDateTime, result.StartDateTime); + Assert.IsFalse(result.IsClosed); + Assert.IsNull(result.StockTakingItems, "StockTakingItems should remain null"); + } + + /// + /// Test to verify property order consistency between serializer and deserializer. + /// + [TestMethod] + public void Diagnostic_VerifyPropertyOrderConsistency() + { + // Get serializer's property order + var serializerType = typeof(AcBinarySerializer); + var metadataCacheField = serializerType.GetField("TypeMetadataCache", + BindingFlags.NonPublic | BindingFlags.Static); + + // Clear cache to force fresh metadata creation + // (This helps ensure we're testing the actual order) + + var type = typeof(SimStockTaking); + var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanRead && p.GetIndexParameters().Length == 0) + .ToArray(); + + Console.WriteLine($"Properties of {type.Name} (reflection order):"); + for (int i = 0; i < props.Length; i++) + { + var prop = props[i]; + Console.WriteLine($" [{i}] {prop.Name} : {prop.PropertyType.Name}"); + } + + // Verify Creator comes BEFORE Created in the reflection order + var creatorIndex = Array.FindIndex(props, p => p.Name == "Creator"); + var createdIndex = Array.FindIndex(props, p => p.Name == "Created"); + var stockTakingItemsIndex = Array.FindIndex(props, p => p.Name == "StockTakingItems"); + + Console.WriteLine($"\nKey indices:"); + Console.WriteLine($" StockTakingItems: {stockTakingItemsIndex}"); + Console.WriteLine($" Creator: {creatorIndex}"); + Console.WriteLine($" Created: {createdIndex}"); + + // The bug scenario: if StockTakingItems is skipped during serialization, + // but the deserializer still expects it at the original index position, + // then Creator (index 3) would be read when expecting StockTakingItems (index 2) + // and Created (index 4) would be read when expecting Creator (index 3) + + Assert.IsTrue(stockTakingItemsIndex >= 0, "StockTakingItems should exist"); + Assert.IsTrue(creatorIndex >= 0, "Creator should exist"); + Assert.IsTrue(createdIndex >= 0, "Created should exist"); + + // In the class definition order: + // StockTakingItems comes BEFORE Creator and Created + Assert.IsTrue(stockTakingItemsIndex < creatorIndex, + "StockTakingItems should come before Creator"); + Assert.IsTrue(creatorIndex < createdIndex, + "Creator should come before Created"); + } + + /// + /// Test multiple StockTakings with null StockTakingItems - exact production scenario. + /// + [TestMethod] + public void Diagnostic_MultipleStockTakings_NullItems() + { + var stockTakings = new List + { + new() + { + Id = 1, + StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc), + IsClosed = false, + Creator = 6, + Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc), + Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc), + StockTakingItems = null + }, + new() + { + Id = 2, + StartDateTime = new DateTime(2025, 1, 23, 9, 0, 0, DateTimeKind.Utc), + IsClosed = true, + Creator = 12, + Created = new DateTime(2025, 1, 23, 14, 0, 0, DateTimeKind.Utc), + Modified = new DateTime(2025, 1, 23, 15, 30, 0, DateTimeKind.Utc), + StockTakingItems = null + } + }; + + var binary = stockTakings.ToBinary(); + Console.WriteLine($"Binary length for 2 StockTakings: {binary.Length}"); + + var result = binary.BinaryTo>(); + + Assert.IsNotNull(result); + Assert.AreEqual(2, result.Count); + + // First item + Assert.AreEqual(1, result[0].Id); + Assert.AreEqual(6, result[0].Creator, "First item Creator should be 6"); + Assert.AreEqual(stockTakings[0].Created, result[0].Created, + $"First item Created mismatch. Expected: {stockTakings[0].Created}, Got: {result[0].Created}"); + + // Second item + Assert.AreEqual(2, result[1].Id); + Assert.AreEqual(12, result[1].Creator, "Second item Creator should be 12"); + Assert.AreEqual(stockTakings[1].Created, result[1].Created, + $"Second item Created mismatch. Expected: {stockTakings[1].Created}, Got: {result[1].Created}"); + } +} diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerGenericTypeTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerGenericTypeTests.cs new file mode 100644 index 0000000..b913756 --- /dev/null +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerGenericTypeTests.cs @@ -0,0 +1,161 @@ +using AyCode.Core.Extensions; +using static AyCode.Core.Tests.Serialization.AcSerializerModels; + +namespace AyCode.Core.Tests.Serialization; + +/// +/// Tests for generic type parameter handling in serialization. +/// +[TestClass] +public class AcBinarySerializerGenericTypeTests +{ + /// + /// CRITICAL REGRESSION TEST: Generic type parameter causing metadata mismatch. + /// + /// The bug pattern: + /// 1. Parent class uses generic type parameter: GenericParent<TItem> where TItem : IGenericItem + /// 2. RegisterMetadataForType uses GetCollectionElementType which returns TItem (the interface/constraint) + /// 3. But serialization uses runtime type (GenericItemImpl) which has MORE properties + /// 4. Property indices in metadata table don't match what's being serialized + /// 5. Deserialization reads wrong property indices ? type mismatch! + /// + [TestMethod] + public void Deserialize_GenericTypeParameter_RuntimeTypeHasMoreProperties() + { + var parent = new ConcreteParent + { + Id = 1, + Name = "Parent", + Creator = 6, + Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc), + Modified = DateTime.UtcNow, + Items = new List + { + new() + { + Id = 10, + Name = "Item1", + ExtraInt = 100, + Created = DateTime.UtcNow.AddHours(-1), + Modified = DateTime.UtcNow, + Description = "Description1" + }, + new() + { + Id = 20, + Name = "Item2", + ExtraInt = 200, + Created = DateTime.UtcNow.AddHours(-2), + Modified = DateTime.UtcNow, + Description = "Description2" + } + } + }; + + var binary = parent.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(1, result.Id); + Assert.AreEqual(6, result.Creator, "Creator should be 6"); + Assert.AreEqual(parent.Created.Ticks, result.Created.Ticks, + $"Created mismatch. Expected: {parent.Created}, Got: {result.Created}"); + + Assert.IsNotNull(result.Items); + Assert.AreEqual(2, result.Items.Count); + + Assert.AreEqual(10, result.Items[0].Id); + Assert.AreEqual(100, result.Items[0].ExtraInt, "ExtraInt should be preserved"); + Assert.AreEqual(parent.Items[0].Created.Ticks, result.Items[0].Created.Ticks, + "Item Created should match"); + } + + /// + /// Test with list of generic parents. + /// + [TestMethod] + public void Deserialize_ListOfGenericParents_AllItemsCorrect() + { + var parents = Enumerable.Range(1, 5).Select(p => new ConcreteParent + { + Id = p, + Name = $"Parent_{p}", + Creator = p, + Created = DateTime.UtcNow.AddDays(-p), + Modified = DateTime.UtcNow, + Items = Enumerable.Range(1, 3).Select(i => new GenericItemImpl + { + Id = p * 100 + i, + Name = $"Item_{p}_{i}", + ExtraInt = p * 10 + i, + Created = DateTime.UtcNow.AddHours(-i), + Modified = DateTime.UtcNow, + Description = $"Desc_{p}_{i}" + }).ToList() + }).ToList(); + + var binary = parents.ToBinary(); + var result = binary.BinaryTo>(); + + Assert.IsNotNull(result); + Assert.AreEqual(5, result.Count); + + for (int p = 0; p < 5; p++) + { + var original = parents[p]; + var deserialized = result[p]; + + Assert.AreEqual(original.Id, deserialized.Id, $"Parent[{p}].Id mismatch"); + Assert.AreEqual(original.Creator, deserialized.Creator, $"Parent[{p}].Creator mismatch"); + Assert.AreEqual(original.Created.Ticks, deserialized.Created.Ticks, + $"Parent[{p}].Created mismatch. Expected: {original.Created}, Got: {deserialized.Created}"); + + Assert.IsNotNull(deserialized.Items, $"Parent[{p}].Items is null"); + Assert.AreEqual(3, deserialized.Items.Count); + + for (int i = 0; i < 3; i++) + { + var origItem = original.Items![i]; + var deserItem = deserialized.Items[i]; + + Assert.AreEqual(origItem.Id, deserItem.Id, $"Parent[{p}].Items[{i}].Id mismatch"); + Assert.AreEqual(origItem.ExtraInt, deserItem.ExtraInt, + $"Parent[{p}].Items[{i}].ExtraInt mismatch"); + Assert.AreEqual(origItem.Created.Ticks, deserItem.Created.Ticks, + $"Parent[{p}].Items[{i}].Created mismatch"); + } + } + } + + /// + /// Diagnostic: Check what PropertyType the reflection returns for generic type parameter. + /// + [TestMethod] + public void Diagnostic_GenericProperty_ReflectionType() + { + var parentType = typeof(ConcreteParent); + var itemsProp = parentType.GetProperty("Items"); + + Assert.IsNotNull(itemsProp); + + var propType = itemsProp.PropertyType; + Console.WriteLine($"PropertyType: {propType}"); + Console.WriteLine($"PropertyType.FullName: {propType.FullName}"); + Console.WriteLine($"IsGenericType: {propType.IsGenericType}"); + + if (propType.IsGenericType) + { + var args = propType.GetGenericArguments(); + Console.WriteLine($"GenericArguments.Length: {args.Length}"); + foreach (var arg in args) + { + Console.WriteLine($" GenericArgument: {arg.FullName}"); + } + } + + Assert.IsTrue(propType.IsGenericType); + var elementType = propType.GetGenericArguments()[0]; + Assert.AreEqual(typeof(GenericItemImpl), elementType, + "Element type should be GenericItemImpl, not IGenericItem"); + } +} diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerNavigationPropertyTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerNavigationPropertyTests.cs new file mode 100644 index 0000000..71154ea --- /dev/null +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerNavigationPropertyTests.cs @@ -0,0 +1,370 @@ +using AyCode.Core.Extensions; +using static AyCode.Core.Tests.Serialization.AcSerializerModels; + +namespace AyCode.Core.Tests.Serialization; + +/// +/// Tests for navigation property serialization issues. +/// +/// CRITICAL BUG REPRODUCTION: +/// When a navigation property (like StockTakingItem.Product) is populated, +/// the serializer writes properties of the navigation target (Product), +/// but these property names were NOT registered in the metadata header! +/// +/// The bug pattern: +/// 1. RegisterMetadataForType walks List<StockTakingItem> and registers StockTakingItem properties +/// 2. StockTakingItem has a "Product" property of type Product - this property NAME is registered +/// 3. BUT Product's own properties (Name, Description, Price, CategoryId) are NOT registered! +/// 4. When Product is NOT NULL at runtime, WriteObject writes Product's property indices +/// 5. GetPropertyNameIndex returns NEW indices that weren't in the header! +/// 6. Deserializer reads property indices that don't exist in its table ? crash/type mismatch +/// +[TestClass] +public class AcBinarySerializerNavigationPropertyTests +{ + /// + /// CRITICAL REGRESSION TEST: Navigation properties causing metadata mismatch. + /// This is the EXACT production scenario: + /// - StockTakingItem.Product is populated by the database query + /// - Product's properties are serialized with wrong indices + /// - Deserializer fails with type mismatch + /// + [TestMethod] + public void Deserialize_NavigationPropertyPopulated_MetadataIncludesNestedType() + { + var parent = new ParentWithNavigatingItems + { + Id = 1, + Name = "Parent", + Creator = 6, // The exact value from production error + Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc), + Modified = DateTime.UtcNow, + Items = new List + { + new() + { + Id = 10, + ParentId = 1, + ProductId = 100, + IsMeasured = true, + Quantity = 50, + Created = DateTime.UtcNow.AddHours(-1), + Modified = DateTime.UtcNow, + // Navigation property IS populated - this is the key! + Product = new ProductEntity + { + Id = 100, + Name = "TestProduct", + Description = "Product description with long text", + Price = 99.99, + CategoryId = 5, + Created = DateTime.UtcNow.AddDays(-30) + } + } + } + }; + + var binary = parent.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(1, result.Id); + Assert.AreEqual(6, result.Creator, "Creator should be 6"); + Assert.AreEqual(parent.Created.Ticks, result.Created.Ticks, + $"Created mismatch. Expected: {parent.Created}, Got: {result.Created}"); + + Assert.IsNotNull(result.Items); + Assert.AreEqual(1, result.Items.Count); + + var item = result.Items[0]; + Assert.AreEqual(10, item.Id); + Assert.AreEqual(100, item.ProductId); + + // Navigation property should be deserialized correctly + Assert.IsNotNull(item.Product, "Product navigation property should not be null"); + Assert.AreEqual(100, item.Product.Id); + Assert.AreEqual("TestProduct", item.Product.Name); + Assert.AreEqual(5, item.Product.CategoryId); + } + + /// + /// Test with multiple items, some with Product populated, some without. + /// This creates a mixed scenario where some items have navigation properties. + /// + [TestMethod] + public void Deserialize_MixedNavigationProperties_AllItemsCorrect() + { + var parent = new ParentWithNavigatingItems + { + Id = 1, + Name = "Parent", + Creator = 6, + Created = DateTime.UtcNow, + Modified = DateTime.UtcNow, + Items = Enumerable.Range(1, 5).Select(i => new ItemWithNavigationProperty + { + Id = i * 10, + ParentId = 1, + ProductId = 100 + i, + IsMeasured = i % 2 == 0, + Quantity = i * 10, + Created = DateTime.UtcNow.AddHours(-i), + Modified = DateTime.UtcNow, + // Only populate Product for even items + Product = i % 2 == 0 ? new ProductEntity + { + Id = 100 + i, + Name = $"Product_{i}", + Description = $"Description for product {i}", + Price = i * 10.5, + CategoryId = i % 3, + Created = DateTime.UtcNow.AddDays(-i) + } : null + }).ToList() + }; + + var binary = parent.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(1, result.Id); + Assert.AreEqual(6, result.Creator); + + Assert.IsNotNull(result.Items); + Assert.AreEqual(5, result.Items.Count); + + for (int i = 1; i <= 5; i++) + { + var item = result.Items[i - 1]; + Assert.AreEqual(i * 10, item.Id, $"Item {i} Id mismatch"); + + if (i % 2 == 0) + { + Assert.IsNotNull(item.Product, $"Item {i} should have Product"); + Assert.AreEqual($"Product_{i}", item.Product.Name); + } + else + { + Assert.IsNull(item.Product, $"Item {i} should not have Product"); + } + } + } + + /// + /// Test with list of parents, each with items with navigation properties. + /// This is the exact production scenario - multiple StockTaking entities + /// each with StockTakingItems that have Product navigation properties. + /// + [TestMethod] + public void Deserialize_ListOfParentsWithNavigationProperties_AllCorrect() + { + var parents = Enumerable.Range(1, 3).Select(p => new ParentWithNavigatingItems + { + Id = p, + Name = $"Parent_{p}", + Creator = p, + Created = DateTime.UtcNow.AddDays(-p), + Modified = DateTime.UtcNow, + Items = Enumerable.Range(1, 2).Select(i => new ItemWithNavigationProperty + { + Id = p * 100 + i, + ParentId = p, + ProductId = 1000 + i, + IsMeasured = true, + Quantity = 10 * i, + Created = DateTime.UtcNow.AddHours(-i), + Modified = DateTime.UtcNow, + Product = new ProductEntity + { + Id = 1000 + i, + Name = $"Product_{p}_{i}", + Description = $"Description {p}_{i}", + Price = (p * 10) + (i * 1.5), + CategoryId = i % 3, + Created = DateTime.UtcNow.AddDays(-10) + } + }).ToList() + }).ToList(); + + var binary = parents.ToBinary(); + var result = binary.BinaryTo>(); + + Assert.IsNotNull(result); + Assert.AreEqual(3, result.Count); + + for (int p = 0; p < 3; p++) + { + var parent = result[p]; + Assert.AreEqual(p + 1, parent.Id, $"Parent[{p}].Id mismatch"); + Assert.AreEqual(p + 1, parent.Creator, $"Parent[{p}].Creator mismatch"); + + Assert.IsNotNull(parent.Items); + Assert.AreEqual(2, parent.Items.Count); + + for (int i = 0; i < 2; i++) + { + var item = parent.Items[i]; + Assert.IsNotNull(item.Product, $"Parent[{p}].Items[{i}].Product should not be null"); + Assert.AreEqual($"Product_{p + 1}_{i + 1}", item.Product.Name); + } + } + } + + /// + /// Test deeply nested navigation properties. + /// Product has a Category, Category has a Parent, etc. + /// + [TestMethod] + public void Deserialize_DeeplyNestedNavigationProperties_AllCorrect() + { + // This tests that the serializer correctly handles navigation properties + // even when they are deeply nested (Product -> Category -> Parent) + var parent = new ParentWithNavigatingItems + { + Id = 1, + Name = "Parent", + Creator = 6, + Created = DateTime.UtcNow, + Modified = DateTime.UtcNow, + Items = new List + { + new() + { + Id = 10, + ParentId = 1, + ProductId = 100, + IsMeasured = true, + Quantity = 50, + Created = DateTime.UtcNow, + Modified = DateTime.UtcNow, + Product = new ProductEntity + { + Id = 100, + Name = "ProductWithDetails", + Description = "Very long description that should be interned", + Price = 123.45, + CategoryId = 10, + Created = DateTime.UtcNow.AddMonths(-6) + } + }, + new() + { + Id = 20, + ParentId = 1, + ProductId = 200, + IsMeasured = false, + Quantity = 25, + Created = DateTime.UtcNow, + Modified = DateTime.UtcNow, + Product = new ProductEntity + { + Id = 200, + Name = "AnotherProduct", + Description = "Another description", + Price = 67.89, + CategoryId = 20, + Created = DateTime.UtcNow.AddMonths(-3) + } + } + } + }; + + var binary = parent.ToBinary(); + + // Log binary size for debugging + Console.WriteLine($"Binary size: {binary.Length} bytes"); + + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(1, result.Id); + Assert.AreEqual(6, result.Creator); + + Assert.IsNotNull(result.Items); + Assert.AreEqual(2, result.Items.Count); + + // First item + Assert.AreEqual(10, result.Items[0].Id); + Assert.IsNotNull(result.Items[0].Product); + Assert.AreEqual("ProductWithDetails", result.Items[0].Product.Name); + Assert.AreEqual(123.45, result.Items[0].Product.Price); + + // Second item + Assert.AreEqual(20, result.Items[1].Id); + Assert.IsNotNull(result.Items[1].Product); + Assert.AreEqual("AnotherProduct", result.Items[1].Product.Name); + Assert.AreEqual(67.89, result.Items[1].Product.Price); + } + + /// + /// Test with same Product instance referenced multiple times. + /// This tests the reference handling with navigation properties. + /// + [TestMethod] + public void Deserialize_SharedNavigationProperty_ReferencesPreserved() + { + // Create a shared Product that is referenced by multiple items + var sharedProduct = new ProductEntity + { + Id = 999, + Name = "SharedProduct", + Description = "This product is shared across items", + Price = 50.00, + CategoryId = 1, + Created = DateTime.UtcNow.AddYears(-1) + }; + + var parent = new ParentWithNavigatingItems + { + Id = 1, + Name = "Parent", + Creator = 6, + Created = DateTime.UtcNow, + Modified = DateTime.UtcNow, + Items = new List + { + new() + { + Id = 10, + ParentId = 1, + ProductId = 999, + IsMeasured = true, + Quantity = 50, + Created = DateTime.UtcNow, + Modified = DateTime.UtcNow, + Product = sharedProduct // Same reference + }, + new() + { + Id = 20, + ParentId = 1, + ProductId = 999, + IsMeasured = false, + Quantity = 75, + Created = DateTime.UtcNow, + Modified = DateTime.UtcNow, + Product = sharedProduct // Same reference + } + } + }; + + var binary = parent.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.IsNotNull(result.Items); + Assert.AreEqual(2, result.Items.Count); + + // Both items should have the same Product values + Assert.IsNotNull(result.Items[0].Product); + Assert.IsNotNull(result.Items[1].Product); + Assert.AreEqual(999, result.Items[0].Product.Id); + Assert.AreEqual(999, result.Items[1].Product.Id); + Assert.AreEqual("SharedProduct", result.Items[0].Product.Name); + Assert.AreEqual("SharedProduct", result.Items[1].Product.Name); + + // With reference handling, they should be the same instance + // (This depends on UseReferenceHandling being enabled) + Console.WriteLine($"Same instance: {ReferenceEquals(result.Items[0].Product, result.Items[1].Product)}"); + } +} diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerNullableTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerNullableTests.cs new file mode 100644 index 0000000..0854d24 --- /dev/null +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerNullableTests.cs @@ -0,0 +1,237 @@ +using AyCode.Core.Extensions; +using static AyCode.Core.Tests.Serialization.AcSerializerModels; +using static AyCode.Core.Tests.Serialization.AcSerializerTestHelper; + +namespace AyCode.Core.Tests.Serialization; + +/// +/// Tests for nullable value type serialization. +/// +[TestClass] +public class AcBinarySerializerNullableTests +{ + [TestMethod] + public void Deserialize_NullableIntProperty_WithValue_RoundTrip() + { + var obj = new TestClassWithNullableProperties { Id = 42, NullableInt = 123, NullableIntNull = null }; + + var binary = obj.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(42, result.Id); + Assert.AreEqual(123, result.NullableInt); + Assert.IsNull(result.NullableIntNull); + } + + [TestMethod] + public void Deserialize_NullableDoubleProperty_WithValue_RoundTrip() + { + var obj = new TestClassWithNullableProperties { Id = 1, NullableDouble = 3.14159, NullableDoubleNull = null }; + + var binary = obj.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(3.14159, result.NullableDouble); + Assert.IsNull(result.NullableDoubleNull); + } + + [TestMethod] + public void Deserialize_NullableDateTimeProperty_WithValue_RoundTrip() + { + var testDate = new DateTime(2024, 12, 25, 10, 30, 45, DateTimeKind.Utc); + var obj = new TestClassWithNullableProperties { Id = 1, NullableDateTime = testDate, NullableDateTimeNull = null }; + + var binary = obj.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(testDate, result.NullableDateTime); + Assert.IsNull(result.NullableDateTimeNull); + } + + [TestMethod] + public void Deserialize_NullableGuidProperty_WithValue_RoundTrip() + { + var testGuid = Guid.NewGuid(); + var obj = new TestClassWithNullableProperties { Id = 1, NullableGuid = testGuid, NullableGuidNull = null }; + + var binary = obj.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(testGuid, result.NullableGuid); + Assert.IsNull(result.NullableGuidNull); + } + + [TestMethod] + public void Deserialize_NullableDecimalProperty_WithValue_RoundTrip() + { + var obj = new TestClassWithNullableProperties { Id = 1, NullableDecimal = 123456.789m, NullableDecimalNull = null }; + + var binary = obj.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(123456.789m, result.NullableDecimal); + Assert.IsNull(result.NullableDecimalNull); + } + + [TestMethod] + public void Deserialize_NullableBoolProperty_WithValue_RoundTrip() + { + var obj = new TestClassWithNullableProperties { Id = 1, NullableBool = true, NullableBoolNull = null }; + + var binary = obj.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(true, result.NullableBool); + Assert.IsNull(result.NullableBoolNull); + } + + [TestMethod] + public void Deserialize_NullableLongProperty_WithValue_RoundTrip() + { + var obj = new TestClassWithNullableProperties { Id = 1, NullableLong = 9876543210L, NullableLongNull = null }; + + var binary = obj.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(9876543210L, result.NullableLong); + Assert.IsNull(result.NullableLongNull); + } + + [TestMethod] + public void Deserialize_AllNullablePropertiesWithValues_RoundTrip() + { + var testDate = new DateTime(2024, 6, 15, 14, 30, 0, DateTimeKind.Utc); + var testGuid = Guid.Parse("12345678-1234-1234-1234-123456789abc"); + + var obj = new TestClassWithNullableProperties + { + Id = 999, + NullableInt = int.MaxValue, + NullableIntNull = 42, + NullableLong = long.MaxValue, + NullableLongNull = 100L, + NullableDouble = double.MaxValue, + NullableDoubleNull = 2.5, + NullableDecimal = decimal.MaxValue, + NullableDecimalNull = 1.1m, + NullableDateTime = testDate, + NullableDateTimeNull = DateTime.UtcNow, + NullableGuid = testGuid, + NullableGuidNull = Guid.NewGuid(), + NullableBool = false, + NullableBoolNull = true + }; + + var binary = obj.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(obj.Id, result.Id); + Assert.AreEqual(obj.NullableInt, result.NullableInt); + Assert.AreEqual(obj.NullableIntNull, result.NullableIntNull); + Assert.AreEqual(obj.NullableLong, result.NullableLong); + Assert.AreEqual(obj.NullableLongNull, result.NullableLongNull); + Assert.AreEqual(obj.NullableDouble, result.NullableDouble); + Assert.AreEqual(obj.NullableDecimalNull, result.NullableDecimalNull); + Assert.AreEqual(obj.NullableDateTime, result.NullableDateTime); + Assert.AreEqual(obj.NullableGuid, result.NullableGuid); + Assert.AreEqual(obj.NullableBool, result.NullableBool); + Assert.AreEqual(obj.NullableBoolNull, result.NullableBoolNull); + } + + [TestMethod] + public void Deserialize_ObjectWithNestedNullableProperties_RoundTrip() + { + var obj = new TestParentWithNullableChild + { + Id = 1, + Name = "Parent", + Child = new TestClassWithNullableProperties + { + Id = 2, + NullableInt = 100, + NullableDouble = 5.5, + NullableDateTime = DateTime.UtcNow, + NullableGuid = Guid.NewGuid() + } + }; + + var binary = obj.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(obj.Id, result.Id); + Assert.AreEqual(obj.Name, result.Name); + Assert.IsNotNull(result.Child); + Assert.AreEqual(obj.Child.NullableInt, result.Child.NullableInt); + Assert.AreEqual(obj.Child.NullableDouble, result.Child.NullableDouble); + } + + [TestMethod] + public void Deserialize_ListOfObjectsWithNullableProperties_RoundTrip() + { + var items = CreateNullablePropertyItems(10); + var binary = items.ToBinary(); + var result = binary.BinaryTo>(); + + Assert.IsNotNull(result); + Assert.AreEqual(10, result.Count); + + for (int i = 0; i < 10; i++) + { + var original = items[i]; + var deserialized = result[i]; + + Assert.AreEqual(original.Id, deserialized.Id, $"Id mismatch at index {i}"); + Assert.AreEqual(original.NullableInt, deserialized.NullableInt, $"NullableInt mismatch at index {i}"); + Assert.AreEqual(original.NullableDouble, deserialized.NullableDouble, $"NullableDouble mismatch at index {i}"); + Assert.AreEqual(original.NullableGuid, deserialized.NullableGuid, $"NullableGuid mismatch at index {i}"); + } + } + + [TestMethod] + public void Deserialize_StockTakingLikeHierarchy_WithNullableProperties_RoundTrip() + { + var stockTaking = CreateStockTaking(2, 2); + + var binary = stockTaking.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(stockTaking.Id, result.Id); + Assert.AreEqual(stockTaking.IsClosed, result.IsClosed); + Assert.AreEqual(stockTaking.Creator, result.Creator); + + Assert.IsNotNull(result.StockTakingItems); + Assert.AreEqual(2, result.StockTakingItems.Count); + + var item0 = result.StockTakingItems[0]; + Assert.IsNotNull(item0.StockTakingItemPallets); + Assert.AreEqual(2, item0.StockTakingItemPallets.Count); + } + + [TestMethod] + public void Deserialize_ListOfStockTakingLikeEntities_RoundTrip() + { + var stockTakings = CreateStockTakingList(2, 1, 1); + var binary = stockTakings.ToBinary(); + var result = binary.BinaryTo>(); + + Assert.IsNotNull(result); + Assert.AreEqual(2, result.Count); + + Assert.AreEqual(1, result[0].Id); + Assert.IsNotNull(result[0].StockTakingItems); + Assert.AreEqual(1, result[0].StockTakingItems.Count); + + Assert.AreEqual(2, result[1].Id); + Assert.IsNotNull(result[1].StockTakingItems); + } +} diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerObjectTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerObjectTests.cs new file mode 100644 index 0000000..4e48ec7 --- /dev/null +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerObjectTests.cs @@ -0,0 +1,130 @@ +using AyCode.Core.Extensions; +using static AyCode.Core.Tests.Serialization.AcSerializerModels; + +namespace AyCode.Core.Tests.Serialization; + +/// +/// Tests for object serialization including nested objects, lists, and dictionaries. +/// +[TestClass] +public class AcBinarySerializerObjectTests +{ + [TestMethod] + public void Serialize_SimpleObject_RoundTrip() + { + var obj = new TestSimpleClass { Id = 42, Name = "Test Object", Value = 3.14, IsActive = true }; + + var binary = obj.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(obj.Id, result.Id); + Assert.AreEqual(obj.Name, result.Name); + Assert.AreEqual(obj.Value, result.Value); + Assert.AreEqual(obj.IsActive, result.IsActive); + } + + [TestMethod] + public void Serialize_NestedObject_RoundTrip() + { + var obj = new TestNestedClass + { + Id = 1, + Name = "Parent", + Child = new TestSimpleClass { Id = 2, Name = "Child", Value = 2.5, IsActive = true } + }; + + var binary = obj.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(obj.Id, result.Id); + Assert.AreEqual(obj.Name, result.Name); + Assert.IsNotNull(result.Child); + Assert.AreEqual(obj.Child.Id, result.Child.Id); + Assert.AreEqual(obj.Child.Name, result.Child.Name); + } + + [TestMethod] + public void Serialize_List_RoundTrip() + { + var list = new List { 1, 2, 3, 4, 5 }; + var binary = list.ToBinary(); + var result = binary.BinaryTo>(); + + Assert.IsNotNull(result); + CollectionAssert.AreEqual(list, result); + } + + [TestMethod] + public void Serialize_ObjectWithList_RoundTrip() + { + var obj = new TestClassWithList { Id = 1, Items = ["Item1", "Item2", "Item3"] }; + + var binary = obj.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(obj.Id, result.Id); + Assert.IsNotNull(result.Items); + CollectionAssert.AreEqual(obj.Items, result.Items); + } + + [TestMethod] + public void Serialize_Dictionary_RoundTrip() + { + var dict = new Dictionary { ["one"] = 1, ["two"] = 2, ["three"] = 3 }; + + var binary = dict.ToBinary(); + var result = binary.BinaryTo>(); + + Assert.IsNotNull(result); + Assert.AreEqual(dict.Count, result.Count); + foreach (var kvp in dict) + { + Assert.IsTrue(result.ContainsKey(kvp.Key)); + Assert.AreEqual(kvp.Value, result[kvp.Key]); + } + } + + [TestMethod] + public void Populate_UpdatesExistingObject() + { + var target = new TestSimpleClass { Id = 0, Name = "Original" }; + var source = new TestSimpleClass { Id = 42, Name = "Updated", Value = 3.14 }; + + var binary = source.ToBinary(); + binary.BinaryTo(target); + + Assert.AreEqual(42, target.Id); + Assert.AreEqual("Updated", target.Name); + Assert.AreEqual(3.14, target.Value); + } + + [TestMethod] + public void PopulateMerge_MergesNestedObjects() + { + var target = new TestNestedClass + { + Id = 1, + Name = "Original", + Child = new TestSimpleClass { Id = 10, Name = "OriginalChild", Value = 1.0 } + }; + + var source = new TestNestedClass + { + Id = 2, + Name = "Updated", + Child = new TestSimpleClass { Id = 20, Name = "UpdatedChild", Value = 2.0 } + }; + + var binary = source.ToBinary(); + binary.BinaryToMerge(target); + + Assert.AreEqual(2, target.Id); + Assert.AreEqual("Updated", target.Name); + Assert.IsNotNull(target.Child); + Assert.AreEqual(20, target.Child.Id); + Assert.AreEqual("UpdatedChild", target.Child.Name); + } +} diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerStringInterningTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerStringInterningTests.cs new file mode 100644 index 0000000..9c8bbca --- /dev/null +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerStringInterningTests.cs @@ -0,0 +1,289 @@ +using AyCode.Core.Extensions; +using AyCode.Core.Serializers.Binaries; +using static AyCode.Core.Tests.Serialization.AcSerializerModels; +using static AyCode.Core.Tests.Serialization.AcSerializerTestHelper; + +namespace AyCode.Core.Tests.Serialization; + +/// +/// Tests for string interning functionality. +/// +[TestClass] +public class AcBinarySerializerStringInterningTests +{ + [TestMethod] + public void Serialize_RepeatedStrings_UsesInterning() + { + var obj = new TestClassWithRepeatedStrings + { + Field1 = "Repeated", + Field2 = "Repeated", + Field3 = "Repeated", + Field4 = "Unique" + }; + + var binaryWithInterning = AcBinarySerializer.Serialize(obj, AcBinarySerializerOptions.Default); + var binaryWithoutInterning = AcBinarySerializer.Serialize(obj, + new AcBinarySerializerOptions { UseStringInterning = false }); + + Assert.IsTrue(binaryWithInterning.Length < binaryWithoutInterning.Length, + $"With interning: {binaryWithInterning.Length}, Without: {binaryWithoutInterning.Length}"); + + var result1 = AcBinaryDeserializer.Deserialize(binaryWithInterning); + var result2 = AcBinaryDeserializer.Deserialize(binaryWithoutInterning); + + Assert.AreEqual(obj.Field1, result1!.Field1); + Assert.AreEqual(obj.Field1, result2!.Field1); + } + + /// + /// REGRESSION TEST: Comprehensive string interning edge cases. + /// + [TestMethod] + public void StringInterning_PropertyNames_MustBeRegisteredDuringDeserialization() + { + var items = Enumerable.Range(0, 10).Select(i => new TestClassWithLongPropertyNames + { + FirstProperty = $"Value1_{i}", + SecondProperty = $"Value2_{i}", + ThirdProperty = $"Value3_{i}", + FourthProperty = $"Value4_{i}" + }).ToList(); + + var binary = items.ToBinary(); + var result = binary.BinaryTo>(); + + Assert.IsNotNull(result); + Assert.AreEqual(10, result.Count); + for (int i = 0; i < 10; i++) + { + Assert.AreEqual($"Value1_{i}", result[i].FirstProperty); + Assert.AreEqual($"Value2_{i}", result[i].SecondProperty); + } + } + + [TestMethod] + public void StringInterning_MixedShortAndLongStrings_HandledCorrectly() + { + var items = Enumerable.Range(0, 20).Select(i => new TestClassWithMixedStrings + { + Id = i, + ShortName = $"A{i % 3}", + LongName = $"LongName_{i % 5}", + Description = $"Description_value_{i % 7}", + Tag = i % 2 == 0 ? "AB" : "XY" + }).ToList(); + + var binary = items.ToBinary(); + var result = binary.BinaryTo>(); + + Assert.IsNotNull(result); + Assert.AreEqual(20, result.Count); + + for (int i = 0; i < 20; i++) + { + Assert.AreEqual(i, result[i].Id); + Assert.AreEqual($"A{i % 3}", result[i].ShortName); + Assert.AreEqual($"LongName_{i % 5}", result[i].LongName); + Assert.AreEqual($"Description_value_{i % 7}", result[i].Description); + } + } + + [TestMethod] + public void StringInterning_DeeplyNestedObjects_MaintainsCorrectOrder() + { + var root = new TestNestedStructure + { + RootName = "RootObject", + Level1Items = Enumerable.Range(0, 5).Select(i => new TestLevel1 + { + Level1Name = $"Level1_{i}", + Level2Items = Enumerable.Range(0, 3).Select(j => new TestLevel2 + { + Level2Name = $"Level2_{i}_{j}", + Value = $"Value_{i * 3 + j}" + }).ToList() + }).ToList() + }; + + var binary = root.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual("RootObject", result.RootName); + Assert.AreEqual(5, result.Level1Items.Count); + + for (int i = 0; i < 5; i++) + { + Assert.AreEqual($"Level1_{i}", result.Level1Items[i].Level1Name); + Assert.AreEqual(3, result.Level1Items[i].Level2Items.Count); + + for (int j = 0; j < 3; j++) + { + Assert.AreEqual($"Level2_{i}_{j}", result.Level1Items[i].Level2Items[j].Level2Name); + } + } + } + + [TestMethod] + public void StringInterning_RepeatedPropertyValuesAcrossObjects_UsesReferences() + { + var items = Enumerable.Range(0, 50).Select(i => new TestClassWithRepeatedValues + { + Id = i, + Status = i % 3 == 0 ? "Pending" : i % 3 == 1 ? "Processing" : "Completed", + Category = i % 5 == 0 ? "CategoryA" : i % 5 == 1 ? "CategoryB" : "CategoryC", + Priority = i % 2 == 0 ? "High" : "Low_Priority_Value" + }).ToList(); + + var binary = items.ToBinary(); + var result = binary.BinaryTo>(); + + Assert.IsNotNull(result); + Assert.AreEqual(50, result.Count); + + for (int i = 0; i < 50; i++) + { + Assert.AreEqual(i, result[i].Id); + var expectedStatus = i % 3 == 0 ? "Pending" : i % 3 == 1 ? "Processing" : "Completed"; + Assert.AreEqual(expectedStatus, result[i].Status, $"Status mismatch at index {i}"); + } + } + + [TestMethod] + public void StringInterning_ManyUniqueStringsFollowedByRepeats_CorrectIndexLookup() + { + var items = new List(); + + for (int i = 0; i < 30; i++) + { + items.Add(new TestClassWithNameValue + { + Name = $"UniqueName_{i:D4}", + Value = $"UniqueValue_{i:D4}" + }); + } + + for (int i = 0; i < 20; i++) + { + items.Add(new TestClassWithNameValue + { + Name = $"UniqueName_{i % 10:D4}", + Value = $"UniqueValue_{(i + 10) % 30:D4}" + }); + } + + var binary = items.ToBinary(); + var result = binary.BinaryTo>(); + + Assert.IsNotNull(result); + Assert.AreEqual(50, result.Count); + + for (int i = 0; i < 30; i++) + { + Assert.AreEqual($"UniqueName_{i:D4}", result[i].Name, $"Name mismatch at index {i}"); + Assert.AreEqual($"UniqueValue_{i:D4}", result[i].Value, $"Value mismatch at index {i}"); + } + + for (int i = 0; i < 20; i++) + { + Assert.AreEqual($"UniqueName_{i % 10:D4}", result[30 + i].Name, $"Name mismatch at index {30 + i}"); + } + } + + [TestMethod] + public void StringInterning_EmptyAndNullStrings_DoNotAffectInternTable() + { + var items = new List(); + + for (int i = 0; i < 25; i++) + { + items.Add(new TestClassWithNullableStrings + { + Id = i, + RequiredName = $"Required_{i:D3}", + OptionalName = i % 3 == 0 ? null : i % 3 == 1 ? "" : $"Optional_{i:D3}", + Description = i % 2 == 0 ? $"Description_{i % 5:D3}" : null + }); + } + + var binary = items.ToBinary(); + var result = binary.BinaryTo>(); + + Assert.IsNotNull(result); + Assert.AreEqual(25, result.Count); + + for (int i = 0; i < 25; i++) + { + Assert.AreEqual(i, result[i].Id); + Assert.AreEqual($"Required_{i:D3}", result[i].RequiredName); + + if (i % 3 == 0) + { + Assert.IsNull(result[i].OptionalName, $"OptionalName at index {i} should be null"); + } + else if (i % 3 == 1) + { + Assert.IsTrue(string.IsNullOrEmpty(result[i].OptionalName), + $"OptionalName at index {i} should be null or empty, got: '{result[i].OptionalName}'"); + } + else + { + Assert.AreEqual($"Optional_{i:D3}", result[i].OptionalName, + $"OptionalName at index {i} mismatch"); + } + } + } + + [TestMethod] + public void StringInterning_ProductionLikeCustomerDto_RoundTrip() + { + var customers = CreateCustomerLikeItems(25); + var binary = customers.ToBinary(); + var result = binary.BinaryTo>(); + + Assert.IsNotNull(result, "Result should not be null - deserialization failed"); + Assert.AreEqual(25, result.Count); + + for (int i = 0; i < 25; i++) + { + Assert.AreEqual(i, result[i].Id, $"Id mismatch at index {i}"); + Assert.AreEqual($"FirstName_{i % 10}", result[i].FirstName, $"FirstName mismatch at index {i}"); + Assert.AreEqual($"LastName_{i % 8}", result[i].LastName, $"LastName mismatch at index {i}"); + Assert.AreEqual($"user{i}@example.com", result[i].Email, $"Email mismatch at index {i}"); + Assert.AreEqual(4, result[i].Attributes.Count, $"Attributes count mismatch at index {i}"); + Assert.AreEqual("DateOfReceipt", result[i].Attributes[0].Key); + Assert.AreEqual("Priority", result[i].Attributes[1].Key); + } + } + + [TestMethod] + public void StringInterning_LargeListWithHighStringReuse_HandlesCorrectly() + { + const int itemCount = 150; + var items = Enumerable.Range(0, itemCount).Select(i => new TestHighReuseDto + { + Id = i, + CategoryCode = $"CAT_{i % 10:D2}", + StatusCode = $"STATUS_{i % 5:D2}", + TypeCode = $"TYPE_{i % 3:D2}", + PriorityCode = i % 2 == 0 ? "PRIORITY_HIGH" : "PRIORITY_LOW", + UniqueField = $"UNIQUE_{i:D4}" + }).ToList(); + + var binary = items.ToBinary(); + var result = binary.BinaryTo>(); + + Assert.IsNotNull(result, "Result should not be null"); + Assert.AreEqual(itemCount, result.Count, $"Expected {itemCount} items"); + + for (int i = 0; i < itemCount; i++) + { + Assert.AreEqual(i, result[i].Id, $"Id mismatch at {i}"); + Assert.AreEqual($"CAT_{i % 10:D2}", result[i].CategoryCode, $"CategoryCode mismatch at {i}"); + Assert.AreEqual($"STATUS_{i % 5:D2}", result[i].StatusCode, $"StatusCode mismatch at {i}"); + Assert.AreEqual($"TYPE_{i % 3:D2}", result[i].TypeCode, $"TypeCode mismatch at {i}"); + Assert.AreEqual($"UNIQUE_{i:D4}", result[i].UniqueField, $"UniqueField mismatch at {i}"); + } + } +} diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerTests.cs deleted file mode 100644 index 8b91592..0000000 --- a/AyCode.Core.Tests/Serialization/AcBinarySerializerTests.cs +++ /dev/null @@ -1,789 +0,0 @@ -using AyCode.Core.Extensions; -using AyCode.Core.Serializers.Binaries; -using AyCode.Core.Tests.TestModels; - -namespace AyCode.Core.Tests.serialization; - -[TestClass] -public class AcBinarySerializerTests -{ - #region Basic Serialization Tests - - [TestMethod] - public void Serialize_Null_ReturnsSingleNullByte() - { - var result = AcBinarySerializer.Serialize(null); - Assert.AreEqual(1, result.Length); - Assert.AreEqual((byte)0, result[0]); // BinaryTypeCode.Null = 0 - } - - [TestMethod] - public void Serialize_Int32_RoundTrip() - { - var value = 12345; - var binary = AcBinarySerializer.Serialize(value); - var result = AcBinaryDeserializer.Deserialize(binary); - Assert.AreEqual(value, result); - } - - [TestMethod] - public void Serialize_Int64_RoundTrip() - { - var value = 123456789012345L; - var binary = AcBinarySerializer.Serialize(value); - var result = AcBinaryDeserializer.Deserialize(binary); - Assert.AreEqual(value, result); - } - - [TestMethod] - public void Serialize_Double_RoundTrip() - { - var value = 3.14159265358979; - var binary = AcBinarySerializer.Serialize(value); - var result = AcBinaryDeserializer.Deserialize(binary); - Assert.AreEqual(value, result); - } - - [TestMethod] - public void Serialize_String_RoundTrip() - { - var value = "Hello, Binary World!"; - var binary = AcBinarySerializer.Serialize(value); - var result = AcBinaryDeserializer.Deserialize(binary); - Assert.AreEqual(value, result); - } - - [TestMethod] - public void Serialize_Boolean_RoundTrip() - { - var trueResult = AcBinaryDeserializer.Deserialize(AcBinarySerializer.Serialize(true)); - var falseResult = AcBinaryDeserializer.Deserialize(AcBinarySerializer.Serialize(false)); - Assert.IsTrue(trueResult); - Assert.IsFalse(falseResult); - } - - [TestMethod] - public void Serialize_DateTime_RoundTrip() - { - var value = new DateTime(2024, 12, 25, 10, 30, 45, DateTimeKind.Utc); - var binary = AcBinarySerializer.Serialize(value); - var result = AcBinaryDeserializer.Deserialize(binary); - Assert.AreEqual(value, result); - } - - [TestMethod] - [DataRow(DateTimeKind.Unspecified)] - [DataRow(DateTimeKind.Utc)] - [DataRow(DateTimeKind.Local)] - public void Serialize_DateTime_PreservesKind(DateTimeKind kind) - { - var value = new DateTime(2024, 12, 25, 10, 30, 45, kind); - var binary = AcBinarySerializer.Serialize(value); - var result = AcBinaryDeserializer.Deserialize(binary); - - Assert.AreEqual(value.Ticks, result.Ticks); - Assert.AreEqual(value.Kind, result.Kind); - } - - [TestMethod] - public void Serialize_Guid_RoundTrip() - { - var value = Guid.NewGuid(); - var binary = AcBinarySerializer.Serialize(value); - var result = AcBinaryDeserializer.Deserialize(binary); - Assert.AreEqual(value, result); - } - - [TestMethod] - public void Serialize_Decimal_RoundTrip() - { - var value = 123456.789012m; - var binary = AcBinarySerializer.Serialize(value); - var result = AcBinaryDeserializer.Deserialize(binary); - Assert.AreEqual(value, result); - } - - [TestMethod] - public void Serialize_TimeSpan_RoundTrip() - { - var value = TimeSpan.FromHours(2.5); - var binary = AcBinarySerializer.Serialize(value); - var result = AcBinaryDeserializer.Deserialize(binary); - Assert.AreEqual(value, result); - } - - [TestMethod] - public void Serialize_DateTimeOffset_RoundTrip() - { - var value = new DateTimeOffset(2024, 12, 25, 10, 30, 45, TimeSpan.FromHours(2)); - var binary = AcBinarySerializer.Serialize(value); - var result = AcBinaryDeserializer.Deserialize(binary); - - // Compare UTC ticks and offset separately since we store UTC ticks - Assert.AreEqual(value.UtcTicks, result.UtcTicks); - Assert.AreEqual(value.Offset, result.Offset); - } - - #endregion - - #region Object Serialization Tests - - [TestMethod] - public void Serialize_SimpleObject_RoundTrip() - { - var obj = new TestSimpleClass - { - Id = 42, - Name = "Test Object", - Value = 3.14, - IsActive = true - }; - - var binary = obj.ToBinary(); - var result = binary.BinaryTo(); - - Assert.IsNotNull(result); - Assert.AreEqual(obj.Id, result.Id); - Assert.AreEqual(obj.Name, result.Name); - Assert.AreEqual(obj.Value, result.Value); - Assert.AreEqual(obj.IsActive, result.IsActive); - } - - [TestMethod] - public void Serialize_NestedObject_RoundTrip() - { - var obj = new TestNestedClass - { - Id = 1, - Name = "Parent", - Child = new TestSimpleClass - { - Id = 2, - Name = "Child", - Value = 2.5, - IsActive = true - } - }; - - var binary = obj.ToBinary(); - var result = binary.BinaryTo(); - - Assert.IsNotNull(result); - Assert.AreEqual(obj.Id, result.Id); - Assert.AreEqual(obj.Name, result.Name); - Assert.IsNotNull(result.Child); - Assert.AreEqual(obj.Child.Id, result.Child.Id); - Assert.AreEqual(obj.Child.Name, result.Child.Name); - } - - [TestMethod] - public void Serialize_List_RoundTrip() - { - var list = new List { 1, 2, 3, 4, 5 }; - var binary = list.ToBinary(); - var result = binary.BinaryTo>(); - - Assert.IsNotNull(result); - CollectionAssert.AreEqual(list, result); - } - - [TestMethod] - public void Serialize_ObjectWithList_RoundTrip() - { - var obj = new TestClassWithList - { - Id = 1, - Items = new List { "Item1", "Item2", "Item3" } - }; - - var binary = obj.ToBinary(); - var result = binary.BinaryTo(); - - Assert.IsNotNull(result); - Assert.AreEqual(obj.Id, result.Id); - Assert.IsNotNull(result.Items); - CollectionAssert.AreEqual(obj.Items, result.Items); - } - - [TestMethod] - public void Serialize_Dictionary_RoundTrip() - { - var dict = new Dictionary - { - ["one"] = 1, - ["two"] = 2, - ["three"] = 3 - }; - - var binary = dict.ToBinary(); - var result = binary.BinaryTo>(); - - Assert.IsNotNull(result); - Assert.AreEqual(dict.Count, result.Count); - foreach (var kvp in dict) - { - Assert.IsTrue(result.ContainsKey(kvp.Key)); - Assert.AreEqual(kvp.Value, result[kvp.Key]); - } - } - - #endregion - - #region Populate Tests - - [TestMethod] - public void Populate_UpdatesExistingObject() - { - var target = new TestSimpleClass { Id = 0, Name = "Original" }; - var source = new TestSimpleClass { Id = 42, Name = "Updated", Value = 3.14 }; - - var binary = source.ToBinary(); - binary.BinaryTo(target); - - Assert.AreEqual(42, target.Id); - Assert.AreEqual("Updated", target.Name); - Assert.AreEqual(3.14, target.Value); - } - - [TestMethod] - public void PopulateMerge_MergesNestedObjects() - { - var target = new TestNestedClass - { - Id = 1, - Name = "Original", - Child = new TestSimpleClass { Id = 10, Name = "OriginalChild", Value = 1.0 } - }; - - var source = new TestNestedClass - { - Id = 2, - Name = "Updated", - Child = new TestSimpleClass { Id = 20, Name = "UpdatedChild", Value = 2.0 } - }; - - var binary = source.ToBinary(); - binary.BinaryToMerge(target); - - Assert.AreEqual(2, target.Id); - Assert.AreEqual("Updated", target.Name); - Assert.IsNotNull(target.Child); - // Child object should be merged, not replaced - Assert.AreEqual(20, target.Child.Id); - Assert.AreEqual("UpdatedChild", target.Child.Name); - } - - #endregion - - #region String Interning Tests - - [TestMethod] - public void Serialize_RepeatedStrings_UsesInterning() - { - var obj = new TestClassWithRepeatedStrings - { - Field1 = "Repeated", - Field2 = "Repeated", - Field3 = "Repeated", - Field4 = "Unique" - }; - - var binaryWithInterning = AcBinarySerializer.Serialize(obj, AcBinarySerializerOptions.Default); - var binaryWithoutInterning = AcBinarySerializer.Serialize(obj, - new AcBinarySerializerOptions { UseStringInterning = false }); - - // With interning should be smaller - Assert.IsTrue(binaryWithInterning.Length < binaryWithoutInterning.Length, - $"With interning: {binaryWithInterning.Length}, Without: {binaryWithoutInterning.Length}"); - - // Both should deserialize correctly - var result1 = AcBinaryDeserializer.Deserialize(binaryWithInterning); - var result2 = AcBinaryDeserializer.Deserialize(binaryWithoutInterning); - - Assert.AreEqual(obj.Field1, result1!.Field1); - Assert.AreEqual(obj.Field1, result2!.Field1); - } - - /// - /// REGRESSION TEST: Comprehensive string interning edge cases. - /// - /// Production bug pattern: "Invalid interned string index: X. Interned strings count: Y" - /// - /// Root causes identified: - /// 1. Property names not being registered in intern table during deserialization - /// 2. String values with same length but different content - /// 3. Nested objects creating complex interning order - /// 4. Collections of objects with repeated property names - /// - [TestMethod] - public void StringInterning_PropertyNames_MustBeRegisteredDuringDeserialization() - { - // This test verifies that property names (>= 4 chars) are properly - // registered in the intern table during deserialization. - // The serializer registers them via WriteString, so deserializer must too. - - var items = Enumerable.Range(0, 10).Select(i => new TestClassWithLongPropertyNames - { - FirstProperty = $"Value1_{i}", - SecondProperty = $"Value2_{i}", - ThirdProperty = $"Value3_{i}", - FourthProperty = $"Value4_{i}" - }).ToList(); - - var binary = items.ToBinary(); - var result = binary.BinaryTo>(); - - Assert.IsNotNull(result); - Assert.AreEqual(10, result.Count); - for (int i = 0; i < 10; i++) - { - Assert.AreEqual($"Value1_{i}", result[i].FirstProperty); - Assert.AreEqual($"Value2_{i}", result[i].SecondProperty); - } - } - - [TestMethod] - public void StringInterning_MixedShortAndLongStrings_HandledCorrectly() - { - // Short strings (< 4 chars) are NOT interned - // Long strings (>= 4 chars) ARE interned - // This creates different traversal patterns - - var items = Enumerable.Range(0, 20).Select(i => new TestClassWithMixedStrings - { - Id = i, - ShortName = $"A{i % 3}", // 2-3 chars, NOT interned - LongName = $"LongName_{i % 5}", // > 4 chars, interned - Description = $"Description_value_{i % 7}", // > 4 chars, interned - Tag = i % 2 == 0 ? "AB" : "XY" // 2 chars, NOT interned - }).ToList(); - - var binary = items.ToBinary(); - var result = binary.BinaryTo>(); - - Assert.IsNotNull(result); - Assert.AreEqual(20, result.Count); - - for (int i = 0; i < 20; i++) - { - Assert.AreEqual(i, result[i].Id); - Assert.AreEqual($"A{i % 3}", result[i].ShortName); - Assert.AreEqual($"LongName_{i % 5}", result[i].LongName); - Assert.AreEqual($"Description_value_{i % 7}", result[i].Description); - } - } - - [TestMethod] - public void StringInterning_DeeplyNestedObjects_MaintainsCorrectOrder() - { - // Complex nested structure where property names and values - // are interleaved in a specific order - - var root = new TestNestedStructure - { - RootName = "RootObject", - Level1Items = Enumerable.Range(0, 5).Select(i => new TestLevel1 - { - Level1Name = $"Level1_{i}", - Level2Items = Enumerable.Range(0, 3).Select(j => new TestLevel2 - { - Level2Name = $"Level2_{i}_{j}", - Value = $"Value_{i * 3 + j}" - }).ToList() - }).ToList() - }; - - var binary = root.ToBinary(); - var result = binary.BinaryTo(); - - Assert.IsNotNull(result); - Assert.AreEqual("RootObject", result.RootName); - Assert.AreEqual(5, result.Level1Items.Count); - - for (int i = 0; i < 5; i++) - { - Assert.AreEqual($"Level1_{i}", result.Level1Items[i].Level1Name); - Assert.AreEqual(3, result.Level1Items[i].Level2Items.Count); - - for (int j = 0; j < 3; j++) - { - Assert.AreEqual($"Level2_{i}_{j}", result.Level1Items[i].Level2Items[j].Level2Name); - } - } - } - - [TestMethod] - public void StringInterning_RepeatedPropertyValuesAcrossObjects_UsesReferences() - { - // When the same string value appears multiple times, - // the serializer writes StringInterned reference instead of the full string. - // The deserializer must look up the correct index. - - var items = Enumerable.Range(0, 50).Select(i => new TestClassWithRepeatedValues - { - Id = i, - Status = i % 3 == 0 ? "Pending" : i % 3 == 1 ? "Processing" : "Completed", - Category = i % 5 == 0 ? "CategoryA" : i % 5 == 1 ? "CategoryB" : "CategoryC", - Priority = i % 2 == 0 ? "High" : "Low_Priority_Value" - }).ToList(); - - var binary = items.ToBinary(); - var result = binary.BinaryTo>(); - - Assert.IsNotNull(result); - Assert.AreEqual(50, result.Count); - - for (int i = 0; i < 50; i++) - { - Assert.AreEqual(i, result[i].Id); - var expectedStatus = i % 3 == 0 ? "Pending" : i % 3 == 1 ? "Processing" : "Completed"; - Assert.AreEqual(expectedStatus, result[i].Status, $"Status mismatch at index {i}"); - } - } - - [TestMethod] - public void StringInterning_ManyUniqueStringsFollowedByRepeats_CorrectIndexLookup() - { - // First create many unique strings (all get registered) - // Then repeat some of them (use StringInterned references) - // This tests the index calculation - - var items = new List(); - - // First 30 items with unique names (all registered as new) - for (int i = 0; i < 30; i++) - { - items.Add(new TestClassWithNameValue - { - Name = $"UniqueName_{i:D4}", - Value = $"UniqueValue_{i:D4}" - }); - } - - // Next 20 items reuse names from first batch (should use StringInterned) - for (int i = 0; i < 20; i++) - { - items.Add(new TestClassWithNameValue - { - Name = $"UniqueName_{i % 10:D4}", // Reuse first 10 names - Value = $"UniqueValue_{(i + 10) % 30:D4}" // Reuse different values - }); - } - - var binary = items.ToBinary(); - var result = binary.BinaryTo>(); - - Assert.IsNotNull(result); - Assert.AreEqual(50, result.Count); - - // Verify first batch - for (int i = 0; i < 30; i++) - { - Assert.AreEqual($"UniqueName_{i:D4}", result[i].Name, $"Name mismatch at index {i}"); - Assert.AreEqual($"UniqueValue_{i:D4}", result[i].Value, $"Value mismatch at index {i}"); - } - - // Verify second batch (reused strings) - for (int i = 0; i < 20; i++) - { - Assert.AreEqual($"UniqueName_{i % 10:D4}", result[30 + i].Name, $"Name mismatch at index {30 + i}"); - } - } - - [TestMethod] - public void StringInterning_EmptyAndNullStrings_DoNotAffectInternTable() - { - // Empty strings use StringEmpty type code - // Null strings use Null type code - // Neither should affect intern table indices - - var items = new List(); - - for (int i = 0; i < 25; i++) - { - items.Add(new TestClassWithNullableStrings - { - Id = i, - RequiredName = $"Required_{i:D3}", - OptionalName = i % 3 == 0 ? null : i % 3 == 1 ? "" : $"Optional_{i:D3}", - Description = i % 2 == 0 ? $"Description_{i % 5:D3}" : null - }); - } - - var binary = items.ToBinary(); - var result = binary.BinaryTo>(); - - Assert.IsNotNull(result); - Assert.AreEqual(25, result.Count); - - for (int i = 0; i < 25; i++) - { - Assert.AreEqual(i, result[i].Id); - Assert.AreEqual($"Required_{i:D3}", result[i].RequiredName); - - if (i % 3 == 0) - { - Assert.IsNull(result[i].OptionalName, $"OptionalName at index {i} should be null"); - } - else if (i % 3 == 1) - { - // Empty string may deserialize as either "" or null depending on implementation - Assert.IsTrue(string.IsNullOrEmpty(result[i].OptionalName), - $"OptionalName at index {i} should be null or empty, got: '{result[i].OptionalName}'"); - } - else - { - Assert.AreEqual($"Optional_{i:D3}", result[i].OptionalName, - $"OptionalName at index {i} mismatch"); - } - } - } - - [TestMethod] - public void StringInterning_ProductionLikeCustomerDto_RoundTrip() - { - // Simulate the CustomerDto structure that causes production issues - // Key characteristics: - // - Many string properties (FirstName, LastName, Email, Company, etc.) - // - GenericAttributes list with repeated Key values - // - List of items with common status/category values - - var customers = Enumerable.Range(0, 25).Select(i => new TestCustomerLikeDto - { - Id = i, - FirstName = $"FirstName_{i % 10}", // 10 unique values - LastName = $"LastName_{i % 8}", // 8 unique values - Email = $"user{i}@example.com", // All unique - Company = $"Company_{i % 5}", // 5 unique values - Department = i % 3 == 0 ? "Sales" : i % 3 == 1 ? "Engineering" : "Marketing", - Role = i % 4 == 0 ? "Admin" : i % 4 == 1 ? "User" : i % 4 == 2 ? "Manager" : "Guest", - Status = i % 2 == 0 ? "Active" : "Inactive", - Attributes = new List - { - new() { Key = "DateOfReceipt", Value = $"{i % 28 + 1}/24/2025 00:27:00" }, - new() { Key = "Priority", Value = (i % 5).ToString() }, - new() { Key = "CustomField1", Value = $"CustomValue_{i % 7}" }, - new() { Key = "CustomField2", Value = i % 2 == 0 ? "Yes" : "No_Value" } - } - }).ToList(); - - var binary = customers.ToBinary(); - var result = binary.BinaryTo>(); - - Assert.IsNotNull(result, "Result should not be null - deserialization failed"); - Assert.AreEqual(25, result.Count); - - for (int i = 0; i < 25; i++) - { - Assert.AreEqual(i, result[i].Id, $"Id mismatch at index {i}"); - Assert.AreEqual($"FirstName_{i % 10}", result[i].FirstName, $"FirstName mismatch at index {i}"); - Assert.AreEqual($"LastName_{i % 8}", result[i].LastName, $"LastName mismatch at index {i}"); - Assert.AreEqual($"user{i}@example.com", result[i].Email, $"Email mismatch at index {i}"); - Assert.AreEqual(4, result[i].Attributes.Count, $"Attributes count mismatch at index {i}"); - - Assert.AreEqual("DateOfReceipt", result[i].Attributes[0].Key); - Assert.AreEqual("Priority", result[i].Attributes[1].Key); - } - } - - [TestMethod] - public void StringInterning_LargeListWithHighStringReuse_HandlesCorrectly() - { - // Large dataset (100+ items) with high string reuse ratio - // This is the scenario that triggers production bugs - - const int itemCount = 150; - var items = Enumerable.Range(0, itemCount).Select(i => new TestHighReuseDto - { - Id = i, - // Property names are reused 150 times (once per object) - CategoryCode = $"CAT_{i % 10:D2}", // 10 unique values, 15x reuse each - StatusCode = $"STATUS_{i % 5:D2}", // 5 unique values, 30x reuse each - TypeCode = $"TYPE_{i % 3:D2}", // 3 unique values, 50x reuse each - PriorityCode = i % 2 == 0 ? "PRIORITY_HIGH" : "PRIORITY_LOW", // 2 values, 75x each - UniqueField = $"UNIQUE_{i:D4}" // All unique, no reuse - }).ToList(); - - var binary = items.ToBinary(); - var result = binary.BinaryTo>(); - - Assert.IsNotNull(result, "Result should not be null"); - Assert.AreEqual(itemCount, result.Count, $"Expected {itemCount} items"); - - // Verify every item - for (int i = 0; i < itemCount; i++) - { - Assert.AreEqual(i, result[i].Id, $"Id mismatch at {i}"); - Assert.AreEqual($"CAT_{i % 10:D2}", result[i].CategoryCode, $"CategoryCode mismatch at {i}"); - Assert.AreEqual($"STATUS_{i % 5:D2}", result[i].StatusCode, $"StatusCode mismatch at {i}"); - Assert.AreEqual($"TYPE_{i % 3:D2}", result[i].TypeCode, $"TypeCode mismatch at {i}"); - Assert.AreEqual($"UNIQUE_{i:D4}", result[i].UniqueField, $"UniqueField mismatch at {i}"); - } - } - - #endregion - - #region Test Models - - private class TestSimpleClass - { - public int Id { get; set; } - public string Name { get; set; } = ""; - public double Value { get; set; } - public bool IsActive { get; set; } - } - - private class TestNestedClass - { - public int Id { get; set; } - public string Name { get; set; } = ""; - public TestSimpleClass? Child { get; set; } - } - - private class TestClassWithList - { - public int Id { get; set; } - public List Items { get; set; } = new(); - } - - private class TestClassWithRepeatedStrings - { - public string Field1 { get; set; } = ""; - public string Field2 { get; set; } = ""; - public string Field3 { get; set; } = ""; - public string Field4 { get; set; } = ""; - } - - // New test models for string interning edge cases - - private class TestClassWithLongPropertyNames - { - public string FirstProperty { get; set; } = ""; - public string SecondProperty { get; set; } = ""; - public string ThirdProperty { get; set; } = ""; - public string FourthProperty { get; set; } = ""; - } - - private class TestClassWithMixedStrings - { - public int Id { get; set; } - public string ShortName { get; set; } = ""; // < 4 chars - public string LongName { get; set; } = ""; // >= 4 chars - public string Description { get; set; } = ""; // >= 4 chars - public string Tag { get; set; } = ""; // < 4 chars - } - - private class TestNestedStructure - { - public string RootName { get; set; } = ""; - public List Level1Items { get; set; } = new(); - } - - private class TestLevel1 - { - public string Level1Name { get; set; } = ""; - public List Level2Items { get; set; } = new(); - } - - private class TestLevel2 - { - public string Level2Name { get; set; } = ""; - public string Value { get; set; } = ""; - } - - private class TestClassWithRepeatedValues - { - public int Id { get; set; } - public string Status { get; set; } = ""; - public string Category { get; set; } = ""; - public string Priority { get; set; } = ""; - } - - private class TestClassWithNameValue - { - public string Name { get; set; } = ""; - public string Value { get; set; } = ""; - } - - private class TestClassWithNullableStrings - { - public int Id { get; set; } - public string RequiredName { get; set; } = ""; - public string? OptionalName { get; set; } - public string? Description { get; set; } - } - - private class TestCustomerLikeDto - { - public int Id { get; set; } - public string FirstName { get; set; } = ""; - public string LastName { get; set; } = ""; - public string Email { get; set; } = ""; - public string Company { get; set; } = ""; - public string Department { get; set; } = ""; - public string Role { get; set; } = ""; - public string Status { get; set; } = ""; - public List Attributes { get; set; } = new(); - } - - private class TestGenericAttribute - { - public string Key { get; set; } = ""; - public string Value { get; set; } = ""; - } - - private class TestHighReuseDto - { - public int Id { get; set; } - public string CategoryCode { get; set; } = ""; - public string StatusCode { get; set; } = ""; - public string TypeCode { get; set; } = ""; - public string PriorityCode { get; set; } = ""; - public string UniqueField { get; set; } = ""; - } - - #endregion - - #region Benchmark Order Tests - - [TestMethod] - public void Serialize_BenchmarkOrder_RoundTrip() - { - // This is the exact same data that causes stack overflow in benchmarks - var order = TestDataFactory.CreateBenchmarkOrder( - itemCount: 3, - palletsPerItem: 2, - measurementsPerPallet: 2, - pointsPerMeasurement: 5); - - // Should not throw stack overflow - var binary = AcBinarySerializer.Serialize(order); - Assert.IsTrue(binary.Length > 0, "Binary data should not be empty"); - - var result = AcBinaryDeserializer.Deserialize(binary); - Assert.IsNotNull(result); - Assert.AreEqual(order.Id, result.Id); - Assert.AreEqual(order.OrderNumber, result.OrderNumber); - Assert.AreEqual(order.Items.Count, result.Items.Count); - } - - [TestMethod] - public void Serialize_BenchmarkOrder_SmallData_RoundTrip() - { - // Smaller test to isolate the issue - var order = TestDataFactory.CreateBenchmarkOrder( - itemCount: 1, - palletsPerItem: 1, - measurementsPerPallet: 1, - pointsPerMeasurement: 1); - - var binary = AcBinarySerializer.Serialize(order); - Assert.IsTrue(binary.Length > 0); - - var result = AcBinaryDeserializer.Deserialize(binary); - Assert.IsNotNull(result); - Assert.AreEqual(order.Id, result.Id); - } - - #endregion -} diff --git a/AyCode.Core.Tests/Serialization/AcSerializerModels.cs b/AyCode.Core.Tests/Serialization/AcSerializerModels.cs new file mode 100644 index 0000000..d48d6d6 --- /dev/null +++ b/AyCode.Core.Tests/Serialization/AcSerializerModels.cs @@ -0,0 +1,633 @@ +namespace AyCode.Core.Tests.Serialization; + +/// +/// Test models for binary serializer tests. +/// +public static class AcSerializerModels +{ + public class TestSimpleClass + { + public int Id { get; set; } + public string Name { get; set; } = ""; + public double Value { get; set; } + public bool IsActive { get; set; } + } + + public class TestNestedClass + { + public int Id { get; set; } + public string Name { get; set; } = ""; + public TestSimpleClass? Child { get; set; } + } + + public class TestClassWithList + { + public int Id { get; set; } + public List Items { get; set; } = new(); + } + + public class TestClassWithRepeatedStrings + { + public string Field1 { get; set; } = ""; + public string Field2 { get; set; } = ""; + public string Field3 { get; set; } = ""; + public string Field4 { get; set; } = ""; + } + + public class TestClassWithLongPropertyNames + { + public string FirstProperty { get; set; } = ""; + public string SecondProperty { get; set; } = ""; + public string ThirdProperty { get; set; } = ""; + public string FourthProperty { get; set; } = ""; + } + + public class TestClassWithMixedStrings + { + public int Id { get; set; } + public string ShortName { get; set; } = ""; + public string LongName { get; set; } = ""; + public string Description { get; set; } = ""; + public string Tag { get; set; } = ""; + } + + public class TestNestedStructure + { + public string RootName { get; set; } = ""; + public List Level1Items { get; set; } = new(); + } + + public class TestLevel1 + { + public string Level1Name { get; set; } = ""; + public List Level2Items { get; set; } = new(); + } + + public class TestLevel2 + { + public string Level2Name { get; set; } = ""; + public string Value { get; set; } = ""; + } + + public class TestClassWithRepeatedValues + { + public int Id { get; set; } + public string Status { get; set; } = ""; + public string Category { get; set; } = ""; + public string Priority { get; set; } = ""; + } + + public class TestClassWithNameValue + { + public string Name { get; set; } = ""; + public string Value { get; set; } = ""; + } + + public class TestClassWithNullableStrings + { + public int Id { get; set; } + public string RequiredName { get; set; } = ""; + public string? OptionalName { get; set; } + public string? Description { get; set; } + } + + public class TestCustomerLikeDto + { + public int Id { get; set; } + public string FirstName { get; set; } = ""; + public string LastName { get; set; } = ""; + public string Email { get; set; } = ""; + public string Company { get; set; } = ""; + public string Department { get; set; } = ""; + public string Role { get; set; } = ""; + public string Status { get; set; } = ""; + public List Attributes { get; set; } = new(); + } + + public class TestAttribute + { + public string Key { get; set; } = ""; + public string Value { get; set; } = ""; + } + + public class TestHighReuseDto + { + public int Id { get; set; } + public string CategoryCode { get; set; } = ""; + public string StatusCode { get; set; } = ""; + public string TypeCode { get; set; } = ""; + public string PriorityCode { get; set; } = ""; + public string UniqueField { get; set; } = ""; + } + + public class TestClassWithNullableProperties + { + public int Id { get; set; } + + public int? NullableInt { get; set; } + public int? NullableIntNull { get; set; } + + public long? NullableLong { get; set; } + public long? NullableLongNull { get; set; } + + public double? NullableDouble { get; set; } + public double? NullableDoubleNull { get; set; } + + public decimal? NullableDecimal { get; set; } + public decimal? NullableDecimalNull { get; set; } + + public DateTime? NullableDateTime { get; set; } + public DateTime? NullableDateTimeNull { get; set; } + + public Guid? NullableGuid { get; set; } + public Guid? NullableGuidNull { get; set; } + + public bool? NullableBool { get; set; } + public bool? NullableBoolNull { get; set; } + } + + public class TestParentWithNullableChild + { + public int Id { get; set; } + public string Name { get; set; } = ""; + public TestClassWithNullableProperties? Child { get; set; } + } + + /// + /// Test model mimicking StockTaking entity structure. + /// /// + public class TestStockTaking + { + public int Id { get; set; } + public DateTime StartDateTime { get; set; } + public bool IsClosed { get; set; } + public int Creator { get; set; } + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + public List? StockTakingItems { get; set; } + } + + /// + /// Test model mimicking StockTakingItem entity structure. + /// /// + public class TestStockTakingItem + { + public int Id { get; set; } + public int StockTakingId { get; set; } + public int ProductId { get; set; } + public bool IsMeasured { get; set; } + public int OriginalStockQuantity { get; set; } + public int MeasuredStockQuantity { get; set; } + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + public List? StockTakingItemPallets { get; set; } + } + + /// + /// Test model mimicking StockTakingItemPallet/MeasuringItemPalletBase entity structure. + /// Contains nullable int properties (CreatorId, ModifierId) that caused the production bug. + /// + public class TestStockTakingItemPallet + { + public int Id { get; set; } + public int StockTakingItemId { get; set; } + public int TrayQuantity { get; set; } + public double TareWeight { get; set; } + public double PalletWeight { get; set; } + public double GrossWeight { get; set; } + public bool IsMeasured { get; set; } + + public int? CreatorId { get; set; } + public int? ModifierId { get; set; } + + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + } + + public class TestEntityWithDateTimeAndInt + { + public int Id { get; set; } + public int IntValue { get; set; } + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + public int StatusCode { get; set; } + public string Name { get; set; } = ""; + } + + public class TestEntityWithManyIntsBeforeDateTime + { + public int Id { get; set; } + public int Value1 { get; set; } + public int Value2 { get; set; } + public int Value3 { get; set; } + public int Value4 { get; set; } + public int Value5 { get; set; } + public DateTime FirstDateTime { get; set; } + public DateTime SecondDateTime { get; set; } + public int FinalValue { get; set; } + } + + public class TestParentEntityWithDateTimeChild + { + public int ParentId { get; set; } + public string ParentName { get; set; } = ""; + public TestEntityWithDateTimeAndInt? Child { get; set; } + } + + public class TestParentWithDateTimeItemCollection + { + public int Id { get; set; } + public string Name { get; set; } = ""; + public DateTime Created { get; set; } + public List? Items { get; set; } + } + + /// + /// Base class to simulate inheritance hierarchy like MgEntityBase. + /// Id property is in base class, so reflection may return it last. + /// + public abstract class TestEntityBase + { + public int Id { get; set; } + } + + /// + /// Simulates MgStockTaking structure with inheritance. + /// Properties are ordered: StartDateTime, IsClosed, StockTakingItems, Creator, Created, Modified + /// Id comes from base class and may appear last in reflection. + /// + public class TestStockTakingWithInheritance : TestEntityBase + { + public DateTime StartDateTime { get; set; } + public bool IsClosed { get; set; } + public List? StockTakingItems { get; set; } + public int Creator { get; set; } + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + } + + /// + /// Simulates StockTakingItem with inheritance. + /// + public class TestStockTakingItemWithInheritance : TestEntityBase + { + public int StockTakingId { get; set; } + public int ProductId { get; set; } + public bool IsMeasured { get; set; } + public int OriginalStockQuantity { get; set; } + public int MeasuredStockQuantity { get; set; } + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + public List? StockTakingItemPallets { get; set; } + } + + /// + /// Simulates MeasuringItemPalletBase with inheritance and ForeignKey pattern. + /// + public class TestStockTakingItemPalletWithInheritance : TestEntityBase + { + public int StockTakingItemId { get; set; } + public int TrayQuantity { get; set; } + public double TareWeight { get; set; } + public double PalletWeight { get; set; } + public double GrossWeight { get; set; } + public bool IsMeasured { get; set; } + public int? CreatorId { get; set; } + public int? ModifierId { get; set; } + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + } + + #region Schema Mismatch Tests + + /// + /// Server-side model with extra properties that client doesn't know about. + /// + public class ServerStockTaking + { + public int Id { get; set; } + public DateTime StartDateTime { get; set; } + public bool IsClosed { get; set; } + public int Creator { get; set; } + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + public List? StockTakingItems { get; set; } + + // Extra properties that client doesn't have + public string? ExtraServerProperty1 { get; set; } + public int ExtraServerProperty2 { get; set; } + } + + /// + /// Server-side item with Product navigation property. + /// + public class ServerStockTakingItem + { + public int Id { get; set; } + public int StockTakingId { get; set; } + public int ProductId { get; set; } + public bool IsMeasured { get; set; } + public int OriginalStockQuantity { get; set; } + public int MeasuredStockQuantity { get; set; } + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + + // Navigation property - this gets serialized on server! + public ServerProductDto? Product { get; set; } + + public List? StockTakingItemPallets { get; set; } + } + + /// + /// Server-side product DTO. + /// + public class ServerProductDto + { + public int Id { get; set; } + public string Name { get; set; } = ""; + public string Description { get; set; } = ""; + public double Price { get; set; } + public int CategoryId { get; set; } + } + + /// + /// Server-side pallet with all properties. + /// + public class ServerStockTakingItemPallet + { + public int Id { get; set; } + public int StockTakingItemId { get; set; } + public int TrayQuantity { get; set; } + public double TareWeight { get; set; } + public double PalletWeight { get; set; } + public double GrossWeight { get; set; } + public bool IsMeasured { get; set; } + public int? CreatorId { get; set; } + public int? ModifierId { get; set; } + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + } + + /// + /// Client-side model - MISSING some properties that server sends. + /// + public class ClientStockTaking + { + public int Id { get; set; } + public DateTime StartDateTime { get; set; } + public bool IsClosed { get; set; } + public int Creator { get; set; } + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + public List? StockTakingItems { get; set; } + // Note: ExtraServerProperty1 and ExtraServerProperty2 are MISSING + } + + /// + /// Client-side item - MISSING Product navigation property. + /// + public class ClientStockTakingItem + { + public int Id { get; set; } + public int StockTakingId { get; set; } + public int ProductId { get; set; } + public bool IsMeasured { get; set; } + public int OriginalStockQuantity { get; set; } + public int MeasuredStockQuantity { get; set; } + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + // Note: Product property is MISSING - server sends it, client skips it + public List? StockTakingItemPallets { get; set; } + } + + /// + /// Client-side pallet. + /// + public class ClientStockTakingItemPallet + { + public int Id { get; set; } + public int StockTakingItemId { get; set; } + public int TrayQuantity { get; set; } + public double TareWeight { get; set; } + public double PalletWeight { get; set; } + public double GrossWeight { get; set; } + public bool IsMeasured { get; set; } + public int? CreatorId { get; set; } + public int? ModifierId { get; set; } + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + } + + #endregion + + #region Circular Reference Test Models + + /// + /// Parent entity with circular reference to child. + /// + public class CircularParent + { + public int Id { get; set; } + public string Name { get; set; } = ""; + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + public int Creator { get; set; } + public List? Children { get; set; } + } + + /// + /// Child entity with back-reference to parent (circular). + /// + public class CircularChild + { + public int Id { get; set; } + public int ParentId { get; set; } + public string Name { get; set; } = ""; + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + + // Back-reference to parent - creates circular reference! + public CircularParent? Parent { get; set; } + + public List? GrandChildren { get; set; } + } + + /// + /// Grandchild with back-reference. + /// + public class CircularGrandChild + { + public int Id { get; set; } + public int ChildId { get; set; } + public int? CreatorId { get; set; } + public int? ModifierId { get; set; } + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + + // Back-reference + public CircularChild? Child { get; set; } + } + + #endregion + + #region Generic Type Parameter Test Models + + /// + /// Interface with fewer properties than implementation. + /// + public interface IGenericItem + { + int Id { get; set; } + string Name { get; set; } + } + + /// + /// Implementation with MORE properties than interface. + /// This is the exact pattern of StockTakingItem. + /// + public class GenericItemImpl : IGenericItem + { + public int Id { get; set; } + public string Name { get; set; } = ""; + + // Extra properties NOT in interface + public int ExtraInt { get; set; } + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + public string Description { get; set; } = ""; + } + + /// + /// Parent class with generic type parameter for items. + /// Similar to MgStockTaking<TStockTakingItem>. + /// + public class GenericParent where TItem : class, IGenericItem + { + public int Id { get; set; } + public string Name { get; set; } = ""; + public int Creator { get; set; } + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + public List? Items { get; set; } + } + + /// + /// Concrete implementation. + /// Similar to StockTaking : MgStockTaking<StockTakingItem>. + /// + public class ConcreteParent : GenericParent + { + } + + #endregion + + #region Navigation Property Test Models + + /// + /// Product entity that is NOT in the parent's generic type hierarchy. + /// + public class ProductEntity + { + public int Id { get; set; } + public string Name { get; set; } = ""; + public string Description { get; set; } = ""; + public double Price { get; set; } + public int CategoryId { get; set; } + public DateTime Created { get; set; } + } + + /// + /// Item entity with navigation property to Product. + /// This simulates StockTakingItem.Product. + /// + public class ItemWithNavigationProperty + { + public int Id { get; set; } + public int ParentId { get; set; } + public int ProductId { get; set; } + public bool IsMeasured { get; set; } + public int Quantity { get; set; } + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + + // Navigation property - NOT in parent's generic type! + // When populated, its properties need to be in metadata table too! + public ProductEntity? Product { get; set; } + } + + /// + /// Parent entity with items that have navigation properties. + /// This simulates StockTaking with StockTakingItems that have Product. + /// + public class ParentWithNavigatingItems + { + public int Id { get; set; } + public string Name { get; set; } = ""; + public int Creator { get; set; } + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + public List? Items { get; set; } + } + + #endregion + + #region Property Order Test Models + + /// + /// Simulates NopCommerce BaseEntity. + /// + public abstract class NopBaseEntity + { + public int Id { get; set; } + } + + /// + /// Simulates MgEntityBase : BaseEntity. + /// + public abstract class SimMgEntityBase : NopBaseEntity + { + public override string ToString() => $"{GetType().Name}; Id: {Id}"; + } + + /// + /// Simulates MgStockTaking with exact property order from production. + /// CRITICAL: Property order in code may differ from reflection order! + /// + public abstract class SimMgStockTaking : SimMgEntityBase + where TStockTakingItem : class + { + public DateTime StartDateTime { get; set; } + public bool IsClosed { get; set; } + public List? StockTakingItems { get; set; } + public int Creator { get; set; } + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + } + + /// + /// Simulates the concrete StockTaking class. + /// + public class SimStockTaking : SimMgStockTaking + { + } + + /// + /// Simulates StockTakingItem with navigation properties. + /// + public class SimStockTakingItem : SimMgEntityBase + { + public int StockTakingId { get; set; } + public int ProductId { get; set; } + public bool IsMeasured { get; set; } + public int OriginalStockQuantity { get; set; } + public int MeasuredStockQuantity { get; set; } + + // Navigation property back to parent (circular!) + public SimStockTaking? StockTaking { get; set; } + + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + } + + #endregion +} diff --git a/AyCode.Core.Tests/Serialization/AcSerializerTestHelper.cs b/AyCode.Core.Tests/Serialization/AcSerializerTestHelper.cs new file mode 100644 index 0000000..991dfda --- /dev/null +++ b/AyCode.Core.Tests/Serialization/AcSerializerTestHelper.cs @@ -0,0 +1,251 @@ +using static AyCode.Core.Tests.Serialization.AcSerializerModels; + +namespace AyCode.Core.Tests.Serialization; + +/// +/// Helper methods for creating test data in serializer tests. +/// +public static class AcSerializerTestHelper +{ + public static List CreateLongPropertyNameItems(int count) + { + return Enumerable.Range(0, count).Select(i => new TestClassWithLongPropertyNames + { + FirstProperty = $"Value1_{i}", + SecondProperty = $"Value2_{i}", + ThirdProperty = $"Value3_{i}", + FourthProperty = $"Value4_{i}" + }).ToList(); + } + + public static List CreateMixedStringItems(int count) + { + return Enumerable.Range(0, count).Select(i => new TestClassWithMixedStrings + { + Id = i, + ShortName = $"A{i % 3}", + LongName = $"LongName_{i % 5}", + Description = $"Description_value_{i % 7}", + Tag = i % 2 == 0 ? "AB" : "XY" + }).ToList(); + } + + public static TestNestedStructure CreateNestedStructure(int level1Count, int level2Count) + { + return new TestNestedStructure + { + RootName = "RootObject", + Level1Items = Enumerable.Range(0, level1Count).Select(i => new TestLevel1 + { + Level1Name = $"Level1_{i}", + Level2Items = Enumerable.Range(0, level2Count).Select(j => new TestLevel2 + { + Level2Name = $"Level2_{i}_{j}", + Value = $"Value_{i * level2Count + j}" + }).ToList() + }).ToList() + }; + } + + public static List CreateRepeatedValueItems(int count) + { + return Enumerable.Range(0, count).Select(i => new TestClassWithRepeatedValues + { + Id = i, + Status = i % 3 == 0 ? "Pending" : i % 3 == 1 ? "Processing" : "Completed", + Category = i % 5 == 0 ? "CategoryA" : i % 5 == 1 ? "CategoryB" : "CategoryC", + Priority = i % 2 == 0 ? "High" : "Low_Priority_Value" + }).ToList(); + } + + public static List CreateNameValueItems(int uniqueCount, int reuseCount) + { + var items = new List(); + + for (int i = 0; i < uniqueCount; i++) + { + items.Add(new TestClassWithNameValue + { + Name = $"UniqueName_{i:D4}", + Value = $"UniqueValue_{i:D4}" + }); + } + + for (int i = 0; i < reuseCount; i++) + { + items.Add(new TestClassWithNameValue + { + Name = $"UniqueName_{i % 10:D4}", + Value = $"UniqueValue_{(i + 10) % uniqueCount:D4}" + }); + } + + return items; + } + + public static List CreateNullableStringItems(int count) + { + return Enumerable.Range(0, count).Select(i => new TestClassWithNullableStrings + { + Id = i, + RequiredName = $"Required_{i:D3}", + OptionalName = i % 3 == 0 ? null : i % 3 == 1 ? "" : $"Optional_{i:D3}", + Description = i % 2 == 0 ? $"Description_{i % 5:D3}" : null + }).ToList(); + } + + public static List CreateCustomerLikeItems(int count) + { + return Enumerable.Range(0, count).Select(i => new TestCustomerLikeDto + { + Id = i, + FirstName = $"FirstName_{i % 10}", + LastName = $"LastName_{i % 8}", + Email = $"user{i}@example.com", + Company = $"Company_{i % 5}", + Department = i % 3 == 0 ? "Sales" : i % 3 == 1 ? "Engineering" : "Marketing", + Role = i % 4 == 0 ? "Admin" : i % 4 == 1 ? "User" : i % 4 == 2 ? "Manager" : "Guest", + Status = i % 2 == 0 ? "Active" : "Inactive", + Attributes = + [ + new() { Key = "DateOfReceipt", Value = $"{i % 28 + 1}/24/2025 00:27:00" }, + new() { Key = "Priority", Value = (i % 5).ToString() }, + new() { Key = "CustomField1", Value = $"CustomValue_{i % 7}" }, + new() { Key = "CustomField2", Value = i % 2 == 0 ? "Yes" : "No_Value" } + ] + }).ToList(); + } + + public static List CreateHighReuseItems(int count) + { + return Enumerable.Range(0, count).Select(i => new TestHighReuseDto + { + Id = i, + CategoryCode = $"CAT_{i % 10:D2}", + StatusCode = $"STATUS_{i % 5:D2}", + TypeCode = $"TYPE_{i % 3:D2}", + PriorityCode = i % 2 == 0 ? "PRIORITY_HIGH" : "PRIORITY_LOW", + UniqueField = $"UNIQUE_{i:D4}" + }).ToList(); + } + + public static List CreateNullablePropertyItems(int count) + { + return Enumerable.Range(1, count).Select(i => new TestClassWithNullableProperties + { + Id = i, + NullableInt = i % 3 == 0 ? null : i * 10, + NullableDouble = i % 2 == 0 ? null : i * 1.5, + NullableDateTime = i % 4 == 0 ? null : DateTime.UtcNow.AddDays(-i), + NullableGuid = i % 5 == 0 ? null : Guid.NewGuid() + }).ToList(); + } + + public static TestStockTaking CreateStockTaking(int itemCount = 2, int palletCount = 2) + { + return new TestStockTaking + { + Id = 1, + StartDateTime = DateTime.UtcNow, + IsClosed = false, + Creator = 100, + Created = DateTime.UtcNow.AddHours(-2), + Modified = DateTime.UtcNow, + StockTakingItems = Enumerable.Range(1, itemCount).Select(i => new TestStockTakingItem + { + Id = i * 10, + StockTakingId = 1, + ProductId = 500 + i, + IsMeasured = i % 2 == 0, + OriginalStockQuantity = 50 * i, + MeasuredStockQuantity = 48 * i, + Created = DateTime.UtcNow.AddHours(-i), + Modified = DateTime.UtcNow, + StockTakingItemPallets = Enumerable.Range(1, palletCount).Select(p => new TestStockTakingItemPallet + { + Id = i * 100 + p, + StockTakingItemId = i * 10, + TrayQuantity = p + 2, + TareWeight = p * 0.5, + PalletWeight = p * 10.0, + GrossWeight = p * 50.0, + IsMeasured = p % 2 == 0, + CreatorId = p % 2 == 0 ? 100 : null, + ModifierId = p % 2 == 1 ? 200 : null, + Created = DateTime.UtcNow, + Modified = DateTime.UtcNow + }).ToList() + }).ToList() + }; + } + + public static List CreateStockTakingList(int count, int itemsPerStock = 1, int palletsPerItem = 1) + { + return Enumerable.Range(1, count).Select(s => new TestStockTaking + { + Id = s, + StartDateTime = DateTime.UtcNow.AddDays(-s), + IsClosed = s % 2 == 0, + Creator = s, + Created = DateTime.UtcNow.AddDays(-s), + Modified = DateTime.UtcNow, + StockTakingItems = Enumerable.Range(1, itemsPerStock).Select(i => new TestStockTakingItem + { + Id = s * 100 + i, + StockTakingId = s, + ProductId = 1000 * s + i, + IsMeasured = i % 2 == 0, + OriginalStockQuantity = 10 * i, + MeasuredStockQuantity = 10 * i, + Created = DateTime.UtcNow, + Modified = DateTime.UtcNow, + StockTakingItemPallets = Enumerable.Range(1, palletsPerItem).Select(p => new TestStockTakingItemPallet + { + Id = s * 1000 + i * 100 + p, + StockTakingItemId = s * 100 + i, + CreatorId = p % 2 == 0 ? s * 10 : null, + ModifierId = p % 2 == 1 ? s * 20 : null, + TrayQuantity = p, + TareWeight = p * 1.0, + PalletWeight = p * 10.0, + GrossWeight = p * 50.0, + IsMeasured = p % 2 == 0, + Created = DateTime.UtcNow, + Modified = DateTime.UtcNow + }).ToList() + }).ToList() + }).ToList(); + } + + public static List CreateDateTimeEntities(int count) + { + return Enumerable.Range(1, count).Select(i => new TestEntityWithDateTimeAndInt + { + Id = i, + IntValue = i * 10, + Created = DateTime.UtcNow.AddDays(-i), + Modified = DateTime.UtcNow.AddHours(-i), + StatusCode = i % 5, + Name = $"Entity_{i}" + }).ToList(); + } + + public static List CreateParentWithDateTimeItems(int parentCount, int itemsPerParent) + { + return Enumerable.Range(1, parentCount).Select(p => new TestParentWithDateTimeItemCollection + { + Id = p, + Name = $"Parent_{p}", + Created = DateTime.UtcNow.AddDays(-p * 10), + Items = Enumerable.Range(1, itemsPerParent).Select(i => new TestEntityWithDateTimeAndInt + { + Id = p * 100 + i, + IntValue = i * 10, + Created = DateTime.UtcNow.AddDays(-i), + Modified = DateTime.UtcNow.AddHours(-i), + StatusCode = i % 3, + Name = $"Parent{p}_Item_{i}" + }).ToList() + }).ToList(); + } +} diff --git a/AyCode.Core.Tests/Serialization/StockTakingTestModels.cs b/AyCode.Core.Tests/Serialization/StockTakingTestModels.cs new file mode 100644 index 0000000..87f21d4 --- /dev/null +++ b/AyCode.Core.Tests/Serialization/StockTakingTestModels.cs @@ -0,0 +1,389 @@ +using AyCode.Interfaces.Entities; +using AyCode.Interfaces.TimeStampInfo; +using Newtonsoft.Json; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq.Expressions; +using static System.Net.Mime.MediaTypeNames; + +namespace AyCode.Core.Tests.Serialization; + + +public abstract partial class BaseEntity : IBaseEntity +{ + /// + /// Gets or sets the entity identifier + /// + public int Id { get; set; } + + public override string ToString() + { + return $"{GetType().Name} [Id: {Id}]"; + } +} + +public interface IBaseEntity //: IEntityInt +{ + public int Id { get; set; } +} + +public abstract class MgEntityBase : BaseEntity, IEntityInt +{ + public override string ToString() + { + return $"{GetType().Name}; Id: {Id}"; + } +} +public interface IMgStockTaking : IEntityInt, ITimeStampInfo +{ + DateTime StartDateTime { get; set; } + bool IsClosed { get; set; } + bool IsReadyForClose(); +} + +public abstract class MgStockTaking : MgEntityBase, IMgStockTaking + where TStockTakingItem : class, IMgStockTakingItem +{ + public DateTime StartDateTime { get; set; } + public bool IsClosed { get; set; } + + public abstract bool IsReadyForClose(); + + public List? StockTakingItems { get; set; } + + public int Creator { get; set; } + public DateTime Created { get; set; } + public DateTime Modified { get; set; } +} + +public interface IMgStockTakingItem : IEntityInt, ITimeStampInfo +{ + int StockTakingId { get; set; } + int ProductId { get; set; } + bool IsMeasured { get; set; } + int OriginalStockQuantity { get; set; } + int MeasuredStockQuantity { get; set; } +} + +public abstract class MgStockTakingItem : MgEntityBase, IMgStockTakingItem + where TStockTaking : class, IMgStockTaking + where TProduct : class, IMgProductDto +{ + public int StockTakingId { get; set; } + public int ProductId { get; set; } + public bool IsMeasured { get; set; } + public int OriginalStockQuantity { get; set; } + public int MeasuredStockQuantity { get; set; } + + public TStockTaking? StockTaking { get; set; } + + public TProduct? Product { get; set; } + + public DateTime Created { get; set; } + public DateTime Modified { get; set; } +} + + +public class StockTaking : MgStockTaking +{ + public override bool IsReadyForClose() + { + if (StockTakingItems == null || StockTakingItems.Count == 0) return false; + return StockTakingItems + .Where(stockTakingItem => stockTakingItem is { IsRequiredForMeasuring: true, IsInvalid: false }) + .All(x => x.IsMeasured); + } +} + + +public class StockTakingItem : MgStockTakingItem +{ + public bool IsMeasurable { get; set; } + + public double OriginalNetWeight { get; set; } + + public double MeasuredNetWeight { get; set; } + + public int InProcessOrdersQuantity { get; set; } + + [NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore] + public int TotalOriginalQuantity => OriginalStockQuantity + InProcessOrdersQuantity; + + [NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore] + public int QuantityDiff => IsMeasured ? MeasuredStockQuantity - TotalOriginalQuantity : 0; + + [NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore] + public double NetWeightDiff => IsMeasurable && IsMeasured ? double.Round(MeasuredNetWeight - OriginalNetWeight, 1) : 0d; + + public List? StockTakingItemPallets { get; set; } + + [NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore] + public bool IsRequiredForMeasuring => !IsInvalid && (TotalOriginalQuantity != 0 || OriginalNetWeight != 0); + + [NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore] + public bool IsInvalid => TotalOriginalQuantity < 0; + + [NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore] + public string DisplayText + { + get + { + if (IsInvalid) return $"[HIBA] {Product!.Name}"; + if (IsMeasured) return $"[KÉSZ] {Product!.Name}"; + return IsRequiredForMeasuring ? $"[KÖT] {Product!.Name}" : $"{Product!.Name}"; + } + } +} + +public class StockTakingItemPallet : MeasuringItemPalletBase +{ + public int StockTakingItemId + { + get => ForeignItemId; + set => ForeignItemId = value; + } + + public StockTakingItem? StockTakingItem { get; set; } + + public override double CalculateNetWeight() => base.CalculateNetWeight(); + + public override bool IsValidSafeMeasuringValues() + { + return StockTakingItemId > 0 && TrayQuantity >= 0 && TareWeight >= 0 && PalletWeight >= 0 && NetWeight >= 0 && GrossWeight >= 0; + } + + public override bool IsValidMeasuringValues(bool isMeasurable) + { + return StockTakingItemId > 0 && TrayQuantity >= 0 && + ((!isMeasurable && NetWeight == 0 && GrossWeight == 0 && PalletWeight == 0 && TareWeight == 0) + || (isMeasurable && NetWeight >= 0 && GrossWeight >= 0 && PalletWeight >= 0 && TareWeight >= 0)); + } + + public override void SetParentPropToNull() => StockTakingItem = null; +} + +public abstract class MeasuringItemPalletBase : MgEntityBase +{ + private double _palletWeight; + private double _grossWeight; + private double _tareWeight; + + protected int ForeignItemId; + + public int ForeignKey => ForeignItemId; + + public int TrayQuantity { get; set; } + + public double TareWeight + { + get => _tareWeight; + set => _tareWeight = double.Round(value, 1); + } + + public double PalletWeight + { + get => _palletWeight; + set => _palletWeight = double.Round(value, 0); + } + + [System.ComponentModel.DataAnnotations.Schema.NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore] + public double NetWeight + { + get => CalculateNetWeight(); + set => throw new Exception($"MeasuringItemPalletBase.NetWeight not set"); + } + + public double GrossWeight + { + get => _grossWeight; + set => _grossWeight = double.Round(value, 1); + } + + public bool IsMeasured { get; set; } + + public int? CreatorId { get; set; } + public int? ModifierId { get; set; } + + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + + public abstract void SetParentPropToNull(); + public void SetForeignKey(int foreignKey) => ForeignItemId = foreignKey; + public virtual double CalculateNetWeight() => double.Round(GrossWeight - PalletWeight - (TareWeight * TrayQuantity), 1); + + public virtual bool IsValidSafeMeasuringValues() + { + return TrayQuantity > 0 && TareWeight >= 0 && PalletWeight >= 0 && NetWeight >= 0 && GrossWeight >= 0; + } + + public virtual bool IsValidMeasuringValues(bool isMeasurable) + { + return TrayQuantity > 0 && + ((!isMeasurable && NetWeight == 0 && GrossWeight == 0 && PalletWeight == 0 && TareWeight == 0) + || (isMeasurable && NetWeight > 0 && GrossWeight > 0 && PalletWeight >= 0 && TareWeight >= 0)); + } + + public bool IsMeasuredAndValid(bool isMeasurable) + { + return Id > 0 && IsMeasured && IsValidMeasuringValues(isMeasurable); + } + + public virtual void SetupCustomItemPalletMeauringValues(bool isMeasurable) + { + if (!isMeasurable) + { + TareWeight = 0; + PalletWeight = 0; + GrossWeight = 0; + } + IsMeasured = IsValidMeasuringValues(isMeasurable); + } + + public override string ToString() + { + return $"{base.ToString()} [ForeignItemId: {ForeignItemId}; IsMeasured: {IsMeasured}; PalletWeight: {PalletWeight}; TareWeight: {TareWeight}; Quantity: {TrayQuantity}; NetWeight: {NetWeight}; GrossWeight: {GrossWeight}]"; + } +} + +public interface IMgProductDto : IEntityInt +{ + int ProductTypeId { get; set; } + int ParentGroupedProductId { get; set; } + + string Name { get; set; } + string ShortDescription { get; set; } + string FullDescription { get; set; } + + int WarehouseId { get; set; } + decimal Price { get; set; } + int StockQuantity { get; set; } + decimal ProductCost { get; set; } + + decimal Weight { get; set; } + decimal Length { get; set; } + decimal Width { get; set; } + decimal Height { get; set; } +} + +public class ProductDto : MgProductDto +{ + public List GenericAttributes { get; set; } + + public ProductDto() :base() + { } + public ProductDto(int productId) : base(productId) + { } + //public ProductDto(Product product) : base(product) + //{ } + + [NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore] + public int AvailableQuantity => StockQuantity; + + public bool HasMeasuringValues() => Id > 0; +} + +public abstract class MgProductDto : MgEntityBase, /*Product,*/ IMgProductDto//IModelDtoBase//, IDiscountSupported +{ + //public int Id { get; set; } + public int ProductTypeId { get; set; } + public int ParentGroupedProductId { get; set; } + + public string Name { get; set; } + public string ShortDescription { get; set; } + public string FullDescription { get; set; } + + public int WarehouseId { get; set; } + public decimal Price { get; set; } + public int StockQuantity { get; set; } + public decimal ProductCost { get; set; } + + public decimal Weight { get; set; } + public decimal Length { get; set; } + public decimal Width { get; set; } + public decimal Height { get; set; } + + public bool Deleted { get; set; } + + public bool SubjectToAcl { get; set; } + public bool LimitedToStores { get; set; } + + protected MgProductDto() :base() + { } + + protected MgProductDto(int productId) + { + Id = productId; + } +} + +public class GenericAttributeDto : MgGenericAttributeDto +{ + +} + +public abstract class MgGenericAttributeDto : GenericAttribute +{ + public GenericAttribute CreateMainEntity() + { + var mainEntity = Activator.CreateInstance(); + CopyDtoValuesToEntity(mainEntity); + + mainEntity.CreatedOrUpdatedDateUTC = DateTime.UtcNow; + return mainEntity; + } + + public void CopyDtoValuesToEntity(GenericAttribute entity) + { + entity.Id = Id; + entity.Key = Key; + entity.Value = Value; + entity.EntityId = EntityId; + entity.KeyGroup = KeyGroup; + entity.StoreId = StoreId; + entity.CreatedOrUpdatedDateUTC = CreatedOrUpdatedDateUTC; + } + + public void CopyEntityValuesToDto(GenericAttribute entity) + { + Id = entity.Id; + Key = entity.Key; + Value = entity.Value; + EntityId = entity.EntityId; + KeyGroup = entity.KeyGroup; + StoreId = entity.StoreId; + CreatedOrUpdatedDateUTC = entity.CreatedOrUpdatedDateUTC; + } +} + +public partial class GenericAttribute : BaseEntity +{ + /// + /// Gets or sets the entity identifier + /// + public int EntityId { get; set; } + + /// + /// Gets or sets the key group + /// + public string KeyGroup { get; set; } + + /// + /// Gets or sets the key + /// + public string Key { get; set; } + + /// + /// Gets or sets the value + /// + public string Value { get; set; } + + /// + /// Gets or sets the store identifier + /// + public int StoreId { get; set; } + + /// + /// Gets or sets the created or updated date + /// + public DateTime? CreatedOrUpdatedDateUTC { get; set; } +} \ No newline at end of file diff --git a/AyCode.Core/Compression/GzipHelper.cs b/AyCode.Core/Compression/GzipHelper.cs new file mode 100644 index 0000000..b76833d --- /dev/null +++ b/AyCode.Core/Compression/GzipHelper.cs @@ -0,0 +1,187 @@ +using System.Buffers; +using System.IO.Compression; +using System.Runtime.CompilerServices; +using System.Text; + +namespace AyCode.Core.Compression; + +/// +/// GZip compression/decompression helper for SignalR message transport. +/// Matches BrotliHelper API to make switching effortless. +/// +public static class GzipHelper +{ + private const int DefaultBufferSize = 4096; + private const int MaxStackAllocSize = 1024; + + #region Compression + + /// + /// Compresses a string using GZip compression with pooled buffers. + /// + public static byte[] Compress(string text, CompressionLevel compressionLevel = CompressionLevel.Optimal) + { + if (string.IsNullOrEmpty(text)) + return []; + + var maxByteCount = Encoding.UTF8.GetMaxByteCount(text.Length); + + if (maxByteCount <= MaxStackAllocSize) + { + Span utf8Bytes = stackalloc byte[maxByteCount]; + var actualLength = Encoding.UTF8.GetBytes(text.AsSpan(), utf8Bytes); + return CompressSpan(utf8Bytes[..actualLength], compressionLevel); + } + + var rentedBuffer = ArrayPool.Shared.Rent(maxByteCount); + try + { + var actualLength = Encoding.UTF8.GetBytes(text.AsSpan(), rentedBuffer); + return CompressSpan(rentedBuffer.AsSpan(0, actualLength), compressionLevel); + } + finally + { + ArrayPool.Shared.Return(rentedBuffer); + } + } + + /// + /// Compresses a byte array using GZip compression. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] Compress(byte[] data, CompressionLevel compressionLevel = CompressionLevel.Optimal) + => data == null || data.Length == 0 ? [] : CompressSpan(data.AsSpan(), compressionLevel); + + /// + /// Compresses a ReadOnlySpan using GZip compression with pooled output buffer. + /// + public static byte[] CompressSpan(ReadOnlySpan data, CompressionLevel compressionLevel = CompressionLevel.Optimal) + { + if (data.IsEmpty) + return []; + + using var outputStream = new MemoryStream(); + using (var gzipStream = new GZipStream(outputStream, compressionLevel, leaveOpen: true)) + { + gzipStream.Write(data); + } + + return outputStream.ToArray(); + } + + #endregion + + #region Decompression + + /// + /// Decompresses GZip-compressed data to a string. + /// + public static string DecompressToString(byte[] compressedData) + { + if (compressedData == null || compressedData.Length == 0) + return string.Empty; + + var decompressedBytes = Decompress(compressedData); + return Encoding.UTF8.GetString(decompressedBytes); + } + + /// + /// Decompresses GZip-compressed data to a byte array. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] Decompress(byte[] compressedData) + { + if (compressedData == null || compressedData.Length == 0) + return []; + + return DecompressCore(compressedData); + } + + /// + /// Decompresses GZip-compressed data from a ReadOnlySpan. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] DecompressSpan(ReadOnlySpan compressedData) + { + if (compressedData.IsEmpty) + return []; + + return DecompressCore(compressedData.ToArray()); + } + + private static byte[] DecompressCore(byte[] compressedData) + { + using var inputStream = new MemoryStream(compressedData, writable: false); + using var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress); + using var outputStream = new MemoryStream(); + + gzipStream.CopyTo(outputStream); + return outputStream.ToArray(); + } + + /// + /// Decompresses to a rented buffer. Caller must return the buffer to ArrayPool. + /// Returns the actual decompressed length. + /// + public static (byte[] Buffer, int Length) DecompressToRentedBuffer(ReadOnlySpan compressedData) + { + if (compressedData.IsEmpty) + return ([], 0); + + var estimatedSize = Math.Max(compressedData.Length * 4, DefaultBufferSize); + var outputBuffer = ArrayPool.Shared.Rent(estimatedSize); + + using var inputStream = new MemoryStream(compressedData.ToArray(), writable: false); + using var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress); + + var totalRead = 0; + int bytesRead; + + while ((bytesRead = gzipStream.Read(outputBuffer.AsSpan(totalRead))) > 0) + { + totalRead += bytesRead; + + if (totalRead >= outputBuffer.Length - DefaultBufferSize) + { + var newBuffer = ArrayPool.Shared.Rent(outputBuffer.Length * 2); + outputBuffer.AsSpan(0, totalRead).CopyTo(newBuffer); + ArrayPool.Shared.Return(outputBuffer); + outputBuffer = newBuffer; + } + } + + return (outputBuffer, totalRead); + } + + #endregion + + #region Utility + + /// + /// Checks if the data appears to be GZip compressed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsGzipCompressed(byte[] data) + { + if (data == null || data.Length < 2) + return false; + + // 0x1F, 0x8B are the fixed gzip header bytes + if (data[0] != 0x1F || data[1] != 0x8B) + return false; + + try + { + using var inputStream = new MemoryStream(data, 0, Math.Min(data.Length, 64), writable: false); + using var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress); + Span buffer = stackalloc byte[1]; + return gzipStream.Read(buffer) >= 0; + } + catch + { + return false; + } + } + + #endregion +} diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs index 2022027..732099b 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs @@ -16,7 +16,10 @@ public static partial class AcBinaryDeserializer private List? _internedStrings; private List? _propertyNames; private Dictionary? _objectReferences; + private Dictionary? _stringCache; private readonly byte _minStringInternLength; + private readonly bool _useStringCaching; + private readonly int _maxCachedStringLength; public bool HasMetadata { get; private set; } public bool HasReferenceHandling { get; private set; } @@ -27,17 +30,25 @@ public static partial class AcBinaryDeserializer public byte MinStringInternLength => _minStringInternLength; public BinaryDeserializationContext(ReadOnlySpan data) + : this(data, AcBinarySerializerOptions.Default) + { + } + + public BinaryDeserializationContext(ReadOnlySpan data, AcBinarySerializerOptions options) { _buffer = data; _position = 0; _internedStrings = null; _propertyNames = null; _objectReferences = null; + _stringCache = null; HasMetadata = false; HasReferenceHandling = false; IsMergeMode = false; RemoveOrphanedItems = false; - _minStringInternLength = AcBinarySerializerOptions.Default.MinStringInternLength; + _minStringInternLength = options.MinStringInternLength; + _useStringCaching = options.UseStringCaching; + _maxCachedStringLength = options.MaxCachedStringLength; } public void ReadHeader() @@ -229,17 +240,51 @@ public static partial class AcBinaryDeserializer return value; } + /// + /// Optimized VarInt reader with fast path for 1-2 byte values (most common case). + /// Uses ZigZag decoding to handle signed integers. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public int ReadVarInt() { var raw = ReadVarUInt(); - var temp = (int)raw; - var value = (temp >> 1) ^ -(temp & 1); + // ZigZag decode: handle full uint range before casting to int + // This correctly handles values like int.MaxValue which encode to uint > int.MaxValue + var value = (int)(raw >> 1) ^ -(int)(raw & 1); return value; } + /// + /// Optimized VarUInt reader with fast path for 1-2 byte values. + /// Most VarInts in real data are small (property indices, array lengths, etc.) + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public uint ReadVarUInt() + { + // Fast path: single byte (0-127) - ~70% of cases + var b0 = _buffer[_position]; + if ((b0 & 0x80) == 0) + { + _position++; + return b0; + } + + // Fast path: two bytes (128-16383) - ~25% of cases + if (_position + 1 < _buffer.Length) + { + var b1 = _buffer[_position + 1]; + if ((b1 & 0x80) == 0) + { + _position += 2; + return (uint)(b0 & 0x7F) | ((uint)b1 << 7); + } + } + + // Slow path: 3+ bytes - ~5% of cases + return ReadVarUIntSlow(); + } + + private uint ReadVarUIntSlow() { uint value = 0; var shift = 0; @@ -309,6 +354,9 @@ public static partial class AcBinaryDeserializer return result; } + /// + /// Read UTF8 string with optional caching for WASM optimization. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public string ReadStringUtf8(int length) { @@ -318,11 +366,67 @@ public static partial class AcBinaryDeserializer } EnsureAvailable(length); + + // WASM optimization: cache short strings to reduce allocations + if (_useStringCaching && length <= _maxCachedStringLength) + { + return ReadStringUtf8Cached(length); + } + var value = Utf8NoBom.GetString(_buffer.Slice(_position, length)); _position += length; return value; } + /// + /// Read string with caching - reduces allocations in WASM. + /// + private string ReadStringUtf8Cached(int length) + { + // Create hash from position and first few bytes for fast lookup + var slice = _buffer.Slice(_position, length); + var hash = ComputeStringHash(slice); + + _stringCache ??= new Dictionary(128); + + if (_stringCache.TryGetValue(hash, out var cached)) + { + // Verify it's actually the same string (hash collision check) + if (cached.Length == length) + { + _position += length; + return cached; + } + } + + var value = Utf8NoBom.GetString(slice); + _stringCache[hash] = value; + _position += length; + return value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ComputeStringHash(ReadOnlySpan data) + { + // Fast hash using first bytes and length + var hash = data.Length; + if (data.Length >= 4) + { + hash = HashCode.Combine(hash, + MemoryMarshal.Read(data)); + } + else if (data.Length >= 2) + { + hash = HashCode.Combine(hash, + MemoryMarshal.Read(data)); + } + else if (data.Length == 1) + { + hash = HashCode.Combine(hash, data[0]); + } + return hash; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Skip(int count) { diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs index 2c8943e..3c1bba2 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs @@ -124,7 +124,38 @@ public static partial class AcBinaryDeserializer public object? GetValue(object target) => _getter(target); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SetValue(object target, object? value) => _setter(target, value); + public void SetValue(object target, object? value) + { + try + { + _setter(target, value); + } + catch (InvalidCastException ex) + { + var valueType = value?.GetType().FullName ?? "null"; + var propType = PropertyType.FullName; + var targetTypeName = target.GetType().FullName; + var underlyingType = Nullable.GetUnderlyingType(PropertyType); + + // Get actual value info for debugging + var valueInfo = value switch + { + null => "null", + int i => $"int:{i}", + long l => $"long:{l}", + DateTime dt => $"DateTime:{dt:O}", + string s => $"string:'{s}'", + _ => $"{value.GetType().Name}:{value}" + }; + + throw new InvalidCastException( + $"Cannot set property '{Name}' on type '{targetTypeName}' - " + + $"PropertyType: '{propType}', ValueType: '{valueType}', Value: {valueInfo}, " + + $"IsNullable: {underlyingType != null}, UnderlyingType: {underlyingType?.FullName ?? "N/A"}, " + + $"IsValueType: {PropertyType.IsValueType}, IsCollection: {IsCollection}, IsComplexType: {IsComplexType}", + ex); + } + } private static bool IsCollectionType(Type type) { @@ -154,8 +185,30 @@ public static partial class AcBinaryDeserializer var valueParam = Expression.Parameter(typeof(object), "value"); var castTarget = Expression.Convert(targetParam, property.DeclaringType!); - var castValue = Expression.Convert(valueParam, property.PropertyType); var propertyAccess = Expression.Property(castTarget, property); + + Expression castValue; + var propertyType = property.PropertyType; + var underlyingType = Nullable.GetUnderlyingType(propertyType); + + if (underlyingType != null) + { + // Nullable value type: first unbox to underlying type, then convert to nullable + // This handles cases where we receive int but need to set int? + var unboxed = Expression.Unbox(valueParam, underlyingType); + castValue = Expression.Convert(unboxed, propertyType); + } + else if (propertyType.IsValueType) + { + // Non-nullable value type: use Unbox for proper unboxing + castValue = Expression.Unbox(valueParam, propertyType); + } + else + { + // Reference type: use TypeAs for safe casting (returns null if incompatible) + castValue = Expression.TypeAs(valueParam, propertyType); + } + var assign = Expression.Assign(propertyAccess, castValue); return Expression.Lambda>(assign, targetParam, valueParam).Compile(); } diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index f8ccc90..72e6b6d 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -110,18 +110,29 @@ public static partial class AcBinaryDeserializer /// Deserialize binary data to object of type T. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static T? Deserialize(byte[] data) => Deserialize(data.AsSpan()); + public static T? Deserialize(byte[] data) => Deserialize(data.AsSpan(), AcBinarySerializerOptions.Default); + + /// + /// Deserialize binary data to object of type T with options. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T? Deserialize(byte[] data, AcBinarySerializerOptions options) => Deserialize(data.AsSpan(), options); /// /// Deserialize binary data to object of type T. /// - public static T? Deserialize(ReadOnlySpan data) + public static T? Deserialize(ReadOnlySpan data) => Deserialize(data, AcBinarySerializerOptions.Default); + + /// + /// Deserialize binary data to object of type T with options. + /// + public static T? Deserialize(ReadOnlySpan data, AcBinarySerializerOptions options) { if (data.Length == 0) return default; if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return default; var targetType = typeof(T); - var context = new BinaryDeserializationContext(data); + var context = new BinaryDeserializationContext(data, options); try { @@ -144,12 +155,18 @@ public static partial class AcBinaryDeserializer /// /// Deserialize binary data to specified type. /// - public static object? Deserialize(ReadOnlySpan data, Type targetType) + public static object? Deserialize(ReadOnlySpan data, Type targetType) + => Deserialize(data, targetType, AcBinarySerializerOptions.Default); + + /// + /// Deserialize binary data to specified type with options. + /// + public static object? Deserialize(ReadOnlySpan data, Type targetType, AcBinarySerializerOptions options) { if (data.Length == 0) return null; if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return null; - var context = new BinaryDeserializationContext(data); + var context = new BinaryDeserializationContext(data, options); try { @@ -172,19 +189,31 @@ public static partial class AcBinaryDeserializer /// Populate existing object from binary data. /// public static void Populate(byte[] data, T target) where T : class - => Populate(data.AsSpan(), target); + => Populate(data.AsSpan(), target, AcBinarySerializerOptions.Default); + + /// + /// Populate existing object from binary data with options. + /// + public static void Populate(byte[] data, T target, AcBinarySerializerOptions options) where T : class + => Populate(data.AsSpan(), target, options); /// /// Populate existing object from binary data. /// public static void Populate(ReadOnlySpan data, T target) where T : class + => Populate(data, target, AcBinarySerializerOptions.Default); + + /// + /// Populate existing object from binary data with options. + /// + public static void Populate(ReadOnlySpan data, T target, AcBinarySerializerOptions options) where T : class { ArgumentNullException.ThrowIfNull(target); if (data.Length == 0) return; if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return; var targetType = target.GetType(); - var context = new BinaryDeserializationContext(data); + var context = new BinaryDeserializationContext(data, options); try { @@ -224,7 +253,7 @@ public static partial class AcBinaryDeserializer /// Populate with merge semantics for IId collections. /// public static void PopulateMerge(ReadOnlySpan data, T target) where T : class - => PopulateMerge(data, target, null); + => PopulateMerge(data, target, AcBinarySerializerOptions.Default); /// /// Populate with merge semantics for IId collections. @@ -239,11 +268,12 @@ public static partial class AcBinaryDeserializer if (data.Length == 0) return; if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return; + var opts = options ?? AcBinarySerializerOptions.Default; var targetType = target.GetType(); - var context = new BinaryDeserializationContext(data) + var context = new BinaryDeserializationContext(data, opts) { IsMergeMode = true, - RemoveOrphanedItems = options?.RemoveOrphanedItems ?? false + RemoveOrphanedItems = opts.RemoveOrphanedItems }; try @@ -489,9 +519,11 @@ public static partial class AcBinaryDeserializer { var propertyCount = (int)context.ReadVarUInt(); var nextDepth = depth + 1; + var targetTypeName = target.GetType().Name; for (int i = 0; i < propertyCount; i++) { + var propertyNameStartPosition = context.Position; string propertyName; if (context.HasMetadata) { @@ -513,10 +545,21 @@ public static partial class AcBinaryDeserializer { propertyName = string.Empty; } + else if (typeCode == BinaryTypeCode.StringInternNew) + { + propertyName = ReadAndRegisterInternedString(ref context); + } + else if (BinaryTypeCode.IsFixStr(typeCode)) + { + // FixStr: short string with length encoded in type code + var length = BinaryTypeCode.DecodeFixStrLength(typeCode); + propertyName = length == 0 ? string.Empty : context.ReadStringUtf8(length); + } else { throw new AcBinaryDeserializationException( - $"Expected string for property name, got: {typeCode}", + $"Expected string for property name, got: {typeCode} (0x{typeCode:X2}) at position {propertyNameStartPosition}. " + + $"Target: {targetTypeName}, PropertyIndex: {i}/{propertyCount}, Depth: {depth}", context.Position, target.GetType()); } } @@ -556,8 +599,27 @@ public static partial class AcBinaryDeserializer } // Default: read value and set (for primitives, strings, null cases) - var value = ReadValue(ref context, propInfo.PropertyType, nextDepth); - propInfo.SetValue(target, value); + var positionBeforeRead = context.Position; + try + { + var value = ReadValue(ref context, propInfo.PropertyType, nextDepth); + propInfo.SetValue(target, value); + } + catch (InvalidCastException ex) + { + // Add context about which property and what byte code was at the read position + throw new AcBinaryDeserializationException( + $"Type mismatch for property '{propertyName}' (index {i}/{propertyCount}) on '{targetTypeName}'. " + + $"Expected type: '{propInfo.PropertyType.FullName}'. " + + $"PeekCode before read: {peekCode} (0x{peekCode:X2}). " + + $"Position before read: {positionBeforeRead}, current: {context.Position}. " + + $"Depth: {depth}. " + + $"All target properties: [{string.Join(", ", metadata.PropertiesArray.Select(p => $"{p.Name}:{p.PropertyType.Name}"))}]. " + + $"Error: {ex.Message}", + positionBeforeRead, + propInfo.PropertyType, + ex); + } } } @@ -621,10 +683,19 @@ public static partial class AcBinaryDeserializer { propertyName = string.Empty; } + else if (typeCode == BinaryTypeCode.StringInternNew) + { + propertyName = ReadAndRegisterInternedString(ref context); + } + else if (BinaryTypeCode.IsFixStr(typeCode)) + { + var length = BinaryTypeCode.DecodeFixStrLength(typeCode); + propertyName = length == 0 ? string.Empty : context.ReadStringUtf8(length); + } else { throw new AcBinaryDeserializationException( - $"Expected string for property name, got: {typeCode}", + $"Expected string for property name, got: {typeCode} (0x{typeCode:X2})", context.Position, targetType); } } @@ -1314,6 +1385,18 @@ public static partial class AcBinaryDeserializer // Just read the index, no registration needed context.ReadVarUInt(); } + else if (nameCode == BinaryTypeCode.StringInternNew) + { + // New interned string - must register even when skipping! + SkipAndRegisterInternedString(ref context); + } + else if (BinaryTypeCode.IsFixStr(nameCode)) + { + // FixStr: short string, just skip the bytes + var length = BinaryTypeCode.DecodeFixStrLength(nameCode); + if (length > 0) + context.Skip(length); + } // StringEmpty doesn't need any action } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs index 7ebfd09..63cdf6e 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs @@ -10,6 +10,11 @@ namespace AyCode.Core.Serializers.Binaries; /// public sealed class AcBinarySerializerOptions : AcSerializerOptions { + /// + /// Cached platform detection - true if running in WebAssembly/Browser environment. + /// + private static readonly bool DetectedIsWasm = OperatingSystem.IsBrowser(); + public override AcSerializerType SerializerType { get; init; } = AcSerializerType.Binary; /// @@ -44,6 +49,37 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions UseReferenceHandling = false }; + /// + /// Options optimized for WASM environment with string caching enabled. + /// + public static readonly AcBinarySerializerOptions WasmOptimized = new() + { + IsWasm = true, + UseStringCaching = true + }; + + /// + /// Whether running in WebAssembly/Browser environment. + /// When true, enables WASM-specific optimizations like string caching. + /// Default: auto-detected via OperatingSystem.IsBrowser() + /// + public bool IsWasm { get; init; } = DetectedIsWasm; + + /// + /// Whether to cache short strings during deserialization to reduce allocations. + /// Most beneficial in WASM where GC is expensive. + /// Auto-enabled when IsWasm is true, can be overridden. + /// Default: follows IsWasm setting + /// + public bool UseStringCaching { get; init; } = DetectedIsWasm; + + /// + /// Maximum string length to cache when UseStringCaching is enabled. + /// Longer strings are not cached to avoid memory bloat. + /// Default: 64 characters + /// + public int MaxCachedStringLength { get; init; } = 64; + /// /// Whether to include metadata header with property names. /// When enabled, property names are stored once and referenced by index. diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs index 9579ea9..c365b47 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs @@ -5,6 +5,7 @@ using AyCode.Core.Tests.TestModels; using AyCode.Services.Server.SignalRs; using AyCode.Services.SignalRs; using MessagePack.Resolvers; +using AyCode.Core.Tests.Serialization; namespace AyCode.Services.Server.Tests.SignalRs; @@ -928,185 +929,68 @@ public abstract class SignalRClientToHubTestBase #endregion - #region Property Mismatch Tests (Server has more properties than Client - tests SkipValue) - + #region StockTaking Production Bug Reproduction + /// - /// REGRESSION TEST: Tests the case where server sends a DTO with more properties than the client knows about. - /// Bug: "Invalid interned string index: 15. Interned strings count: 12" - /// Root cause: When deserializing, unknown properties are skipped via SkipValue(), but the skipped - /// string values were not being registered in the intern table, causing index mismatch for later StringInterned references. + /// CRITICAL PRODUCTION BUG TEST: Reproduces the exact GetStockTakings(false) scenario. + /// Bug: "Type mismatch for property 'Created' (index 3/5) on 'StockTaking'" /// - /// This test simulates the production bug where CustomerDto had properties on server - /// that the client didn't have defined. + /// Root cause hypothesis: When IsClosed=false, the serializer skips it (default value optimization), + /// but the first item has IsClosed=false while others have IsClosed=true. + /// This may cause property index mismatch between server and client. + /// Uses the REAL StockTaking model from StockTakingTestModels.cs /// [TestMethod] - public async Task PropertyMismatch_ServerHasMoreProperties_DeserializesCorrectly() + public async Task GetStockTakings_WithNullItems_RoundTrip() { - // Arrange: Create "server" DTO with many properties - var serverDto = new ServerCustomerDto - { - Id = 1, - FirstName = "John", - LastName = "Smith", - Email = "john.smith@example.com", - Phone = "+1-555-1234", - Address = "123 Main Street", - City = "New York", - Country = "USA", - PostalCode = "10001", - Company = "Acme Corp", - Department = "Engineering", - Notes = "VIP customer with special requirements", - Status = TestStatus.Active, - IsVerified = true, - LoginCount = 42, - Balance = 1234.56m - }; + // Simulate production: loadRelations = false + var result = await _client.PostDataAsync>( + TestSignalRTags.GetStockTakings, false); - // Act: Send server DTO, receive client DTO (fewer properties) - // This simulates the real bug scenario - var result = await _client.PostDataAsync( - TestSignalRTags.PropertyMismatchParam, serverDto); - - // Assert: Client should receive only the properties it knows about - Assert.IsNotNull(result, "Result should not be null - deserialization should succeed even with unknown properties"); - Assert.AreEqual(1, result.Id); - Assert.AreEqual("John", result.FirstName); - Assert.AreEqual("Smith", result.LastName); - } - - /// - /// REGRESSION TEST: Tests a list of DTOs with property mismatch. - /// This more closely simulates the production bug with GetMeasuringUsers returning List<CustomerDto>. - /// - [TestMethod] - public async Task PropertyMismatch_ListOfDtos_WithManyProperties_DeserializesCorrectly() - { - // Arrange: Create list of "server" DTOs with many string properties - var serverDtos = Enumerable.Range(0, 25).Select(i => new ServerCustomerDto - { - Id = i, - FirstName = $"FirstName_{i % 10}", // 10 unique values (will be interned) - LastName = $"LastName_{i % 8}", // 8 unique values - Email = $"user{i}@example.com", - Phone = $"+1-555-{i:D4}", - Address = $"Address_{i % 5}", // 5 unique values - City = i % 3 == 0 ? "New York" : i % 3 == 1 ? "Los Angeles" : "Chicago", - Country = "USA", - PostalCode = $"{10000 + i}", - Company = $"Company_{i % 6}", // 6 unique values - Department = i % 4 == 0 ? "Engineering" : i % 4 == 1 ? "Sales" : i % 4 == 2 ? "Marketing" : "Support", - Notes = $"Notes for customer {i}", - Status = (TestStatus)(i % 5), - IsVerified = i % 2 == 0, - LoginCount = i * 10, - Balance = i * 100.50m - }).ToList(); - - // Act: Send list of server DTOs, receive list of client DTOs - var result = await _client.PostDataAsync, List>( - TestSignalRTags.PropertyMismatchListParam, serverDtos); - - // Assert Assert.IsNotNull(result, "Result should not be null"); - Assert.AreEqual(serverDtos.Count, result.Count, $"Expected {serverDtos.Count} items"); + Assert.AreEqual(5, result.Count, "Should have 5 StockTakings from production DB"); - for (int i = 0; i < serverDtos.Count; i++) + // Verify first item (IsClosed = false - this is where the bug occurs!) + var first = result[0]; + Assert.AreEqual(7, first.Id, "First Id should be 7"); + Assert.AreEqual(6, first.Creator, + $"First Creator should be 6, got {first.Creator}. " + + "If this is a very large number, DateTime bytes were interpreted as int!"); + Assert.IsFalse(first.IsClosed, "First IsClosed should be false"); + Assert.IsNull(first.StockTakingItems, "StockTakingItems should be null when loadRelations=false"); + + // Verify second item (IsClosed = true) + var second = result[1]; + Assert.AreEqual(6, second.Id, "Second Id should be 6"); + Assert.AreEqual(6, second.Creator, "Second Creator should be 6"); + Assert.IsTrue(second.IsClosed, "Second IsClosed should be true"); + + // Verify all items have correct Creator = 6 + foreach (var item in result) { - Assert.AreEqual(serverDtos[i].Id, result[i].Id, $"Id mismatch at index {i}"); - Assert.AreEqual(serverDtos[i].FirstName, result[i].FirstName, - $"FirstName mismatch at index {i}: expected '{serverDtos[i].FirstName}', got '{result[i].FirstName}'"); - Assert.AreEqual(serverDtos[i].LastName, result[i].LastName, - $"LastName mismatch at index {i}: expected '{serverDtos[i].LastName}', got '{result[i].LastName}'"); + Assert.AreEqual(6, item.Creator, + $"StockTaking Id={item.Id}: Creator should be 6, got {item.Creator}"); } } /// - /// REGRESSION TEST: Tests nested objects being skipped when client doesn't know about them. + /// Test with loadRelations = true (StockTakingItems is empty list, not null). /// [TestMethod] - public async Task PropertyMismatch_NestedObjectsSkipped_DeserializesCorrectly() + public async Task GetStockTakings_WithEmptyItems_RoundTrip() { - // Arrange: Server order with nested customer object - var serverOrder = new ServerOrderWithExtras + var result = await _client.PostDataAsync>( + TestSignalRTags.GetStockTakings, true); + + Assert.IsNotNull(result, "Result should not be null"); + Assert.AreEqual(5, result.Count, "Should have 5 StockTakings"); + + foreach (var item in result) { - Id = 100, - OrderNumber = "ORD-2024-001", - TotalAmount = 999.99m, - Customer = new ServerCustomerDto - { - Id = 1, - FirstName = "John", - LastName = "Doe", - Email = "john@example.com", - Phone = "+1-555-0001" - }, - RelatedCustomers = - [ - new ServerCustomerDto { Id = 2, FirstName = "Jane", LastName = "Smith", Email = "jane@example.com" }, - new ServerCustomerDto { Id = 3, FirstName = "Bob", LastName = "Wilson", Email = "bob@example.com" } - ], - InternalNotes = "Priority processing required", - ProcessingCode = "RUSH-001" - }; - - // Act: Send server order, receive simplified client order - var result = await _client.PostDataAsync( - TestSignalRTags.PropertyMismatchNestedParam, serverOrder); - - // Assert: Client should receive only basic order info - Assert.IsNotNull(result); - Assert.AreEqual(100, result.Id); - Assert.AreEqual("ORD-2024-001", result.OrderNumber); - Assert.AreEqual(999.99m, result.TotalAmount); - } - - /// - /// REGRESSION TEST: Large list with nested objects being skipped. - /// This is the most comprehensive test for the SkipValue string interning bug. - /// - [TestMethod] - public async Task PropertyMismatch_LargeListWithNestedObjects_DeserializesCorrectly() - { - // Arrange: Create 50 orders with nested customers - var serverOrders = Enumerable.Range(0, 50).Select(i => new ServerOrderWithExtras - { - Id = i, - OrderNumber = $"ORD-{i:D4}", - TotalAmount = i * 100.50m, - Customer = new ServerCustomerDto - { - Id = i * 100, - FirstName = $"Customer_{i % 10}", - LastName = $"LastName_{i % 8}", - Email = $"customer{i}@example.com", - Company = $"Company_{i % 5}" - }, - RelatedCustomers = Enumerable.Range(0, i % 3 + 1).Select(j => new ServerCustomerDto - { - Id = i * 100 + j, - FirstName = $"Related_{j}", - LastName = $"Contact_{i % 4}", - Email = $"related{i}_{j}@example.com" - }).ToList(), - InternalNotes = $"Notes for order {i}", - ProcessingCode = $"CODE-{i % 10}" - }).ToList(); - - // Act - var result = await _client.PostDataAsync, List>( - TestSignalRTags.PropertyMismatchNestedListParam, serverOrders); - - // Assert - Assert.IsNotNull(result, "Result should not be null - SkipValue should correctly handle unknown nested objects"); - Assert.AreEqual(serverOrders.Count, result.Count); - - for (int i = 0; i < serverOrders.Count; i++) - { - Assert.AreEqual(serverOrders[i].Id, result[i].Id, $"Id mismatch at index {i}"); - Assert.AreEqual(serverOrders[i].OrderNumber, result[i].OrderNumber, - $"OrderNumber mismatch at index {i}: expected '{serverOrders[i].OrderNumber}', got '{result[i].OrderNumber}'"); - Assert.AreEqual(serverOrders[i].TotalAmount, result[i].TotalAmount, $"TotalAmount mismatch at index {i}"); + Assert.AreEqual(6, item.Creator, + $"StockTaking Id={item.Id}: Creator should be 6, got {item.Creator}"); + Assert.IsNotNull(item.StockTakingItems, + $"StockTaking Id={item.Id}: StockTakingItems should not be null when loadRelations=true"); } } diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Binary.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Binary.cs index 21e2fc7..f433de1 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Binary.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Binary.cs @@ -22,5 +22,142 @@ public class SignalRDataSourceTests_List_Binary : SignalRDataSourceTestBase await base.LoadDataSource_InvokesOnDataSourceLoaded(); [TestMethod] public override async Task LoadDataSource_MultipleCalls_RefreshesData() => await base.LoadDataSource_MultipleCalls_RefreshesData(); - // ... (repeat for all other test methods) + [TestMethod] + public override async Task LoadDataSourceAsync_LoadsDataViaCallback() => await base.LoadDataSourceAsync_LoadsDataViaCallback(); + [TestMethod] + public override async Task LoadItem_ReturnsSingleItem() => await base.LoadItem_ReturnsSingleItem(); + [TestMethod] + public override async Task LoadItem_AddsToDataSource_WhenNotExists() => await base.LoadItem_AddsToDataSource_WhenNotExists(); + [TestMethod] + public override async Task LoadItem_UpdatesExisting_WhenAlreadyLoaded() => await base.LoadItem_UpdatesExisting_WhenAlreadyLoaded(); + [TestMethod] + public override async Task LoadItem_InvokesOnDataSourceItemChanged() => await base.LoadItem_InvokesOnDataSourceItemChanged(); + [TestMethod] + public override async Task LoadItem_ReturnsNull_WhenNotFound() => await base.LoadItem_ReturnsNull_WhenNotFound(); + [TestMethod] + public override async Task Add_WithAutoSave_AddsItem() => await base.Add_WithAutoSave_AddsItem(); + [TestMethod] + public override void Add_WithoutAutoSave_AddsToTrackingOnly() => base.Add_WithoutAutoSave_AddsToTrackingOnly(); + [TestMethod] + public override void Add_DuplicateId_ThrowsException() => base.Add_DuplicateId_ThrowsException(); + [TestMethod] + public override void Add_DefaultId_ThrowsException() => base.Add_DefaultId_ThrowsException(); + [TestMethod] + public override void AddRange_AddsMultipleItems() => base.AddRange_AddsMultipleItems(); + [TestMethod] + public override async Task AddOrUpdate_AddsNew_WhenNotExists() => await base.AddOrUpdate_AddsNew_WhenNotExists(); + [TestMethod] + public override async Task AddOrUpdate_UpdatesExisting_WhenExists() => await base.AddOrUpdate_UpdatesExisting_WhenExists(); + [TestMethod] + public override void Insert_AtIndex_InsertsCorrectly() => base.Insert_AtIndex_InsertsCorrectly(); + [TestMethod] + public override async Task Insert_WithAutoSave_SavesImmediately() => await base.Insert_WithAutoSave_SavesImmediately(); + [TestMethod] + public override async Task Update_ByIndex_UpdatesCorrectly() => await base.Update_ByIndex_UpdatesCorrectly(); + [TestMethod] + public override async Task Update_ByItem_UpdatesCorrectly() => await base.Update_ByItem_UpdatesCorrectly(); + [TestMethod] + public override void Indexer_Set_TracksUpdate() => base.Indexer_Set_TracksUpdate(); + [TestMethod] + public override async Task Remove_ById_RemovesItem() => await base.Remove_ById_RemovesItem(); + [TestMethod] + public override async Task Remove_ByItem_RemovesItem() => await base.Remove_ByItem_RemovesItem(); + [TestMethod] + public override void Remove_WithoutAutoSave_TracksRemoval() => base.Remove_WithoutAutoSave_TracksRemoval(); + [TestMethod] + public override void RemoveAt_RemovesCorrectItem() => base.RemoveAt_RemovesCorrectItem(); + [TestMethod] + public override async Task RemoveAt_WithAutoSave_SavesImmediately() => await base.RemoveAt_WithAutoSave_SavesImmediately(); + [TestMethod] + public override void TryRemove_ReturnsTrue_WhenExists() => base.TryRemove_ReturnsTrue_WhenExists(); + [TestMethod] + public override void TryRemove_ReturnsFalse_WhenNotExists() => base.TryRemove_ReturnsFalse_WhenNotExists(); + [TestMethod] + public override async Task SaveChanges_SavesTrackedItems() => await base.SaveChanges_SavesTrackedItems(); + [TestMethod] + public override async Task SaveChangesAsync_ClearsTracking() => await base.SaveChangesAsync_ClearsTracking(); + [TestMethod] + public override async Task SaveItem_ById_SavesSpecificItem() => await base.SaveItem_ById_SavesSpecificItem(); + [TestMethod] + public override async Task SaveItem_WithTrackingState_SavesCorrectly() => await base.SaveItem_WithTrackingState_SavesCorrectly(); + [TestMethod] + public override void SetTrackingStateToUpdate_MarksItemForUpdate() => base.SetTrackingStateToUpdate_MarksItemForUpdate(); + [TestMethod] + public override void SetTrackingStateToUpdate_DoesNotChangeAddState() => base.SetTrackingStateToUpdate_DoesNotChangeAddState(); + [TestMethod] + public override void TryGetTrackingItem_ReturnsTrue_WhenTracked() => base.TryGetTrackingItem_ReturnsTrue_WhenTracked(); + [TestMethod] + public override void TryGetTrackingItem_ReturnsFalse_WhenNotTracked() => base.TryGetTrackingItem_ReturnsFalse_WhenNotTracked(); + [TestMethod] + public override void TryRollbackItem_RevertsAddedItem() => base.TryRollbackItem_RevertsAddedItem(); + [TestMethod] + public override async Task TryRollbackItem_RevertsUpdatedItem() => await base.TryRollbackItem_RevertsUpdatedItem(); + [TestMethod] + public override void Rollback_RevertsAllChanges() => base.Rollback_RevertsAllChanges(); + [TestMethod] + public override async Task Count_ReturnsCorrectValue() => await base.Count_ReturnsCorrectValue(); + [TestMethod] + public override void Clear_RemovesAllItems() => base.Clear_RemovesAllItems(); + [TestMethod] + public override void Clear_WithoutClearingTracking_PreservesTracking() => base.Clear_WithoutClearingTracking_PreservesTracking(); + [TestMethod] + public override async Task Contains_ReturnsTrue_WhenItemExists() => await base.Contains_ReturnsTrue_WhenItemExists(); + [TestMethod] + public override void Contains_ReturnsFalse_WhenItemNotExists() => base.Contains_ReturnsFalse_WhenItemNotExists(); + [TestMethod] + public override async Task IndexOf_ReturnsCorrectIndex() => await base.IndexOf_ReturnsCorrectIndex(); + [TestMethod] + public override void IndexOf_ById_ReturnsCorrectIndex() => base.IndexOf_ById_ReturnsCorrectIndex(); + [TestMethod] + public override void TryGetIndex_ReturnsTrue_WhenExists() => base.TryGetIndex_ReturnsTrue_WhenExists(); + [TestMethod] + public override async Task TryGetValue_ReturnsItem_WhenExists() => await base.TryGetValue_ReturnsItem_WhenExists(); + [TestMethod] + public override void TryGetValue_ReturnsFalse_WhenNotExists() => base.TryGetValue_ReturnsFalse_WhenNotExists(); + [TestMethod] + public override async Task CopyTo_CopiesAllItems() => await base.CopyTo_CopiesAllItems(); + [TestMethod] + public override async Task GetEnumerator_EnumeratesAllItems() => await base.GetEnumerator_EnumeratesAllItems(); + [TestMethod] + public override async Task AsReadOnly_ReturnsReadOnlyCollection() => await base.AsReadOnly_ReturnsReadOnlyCollection(); + [TestMethod] + public override async Task SetWorkingReferenceList_SetsNewInnerList() => await base.SetWorkingReferenceList_SetsNewInnerList(); + [TestMethod] + public override async Task GetReferenceInnerList_ReturnsInnerList() => await base.GetReferenceInnerList_ReturnsInnerList(); + [TestMethod] + public override void IsSyncing_IsFalse_Initially() => base.IsSyncing_IsFalse_Initially(); + [TestMethod] + public override async Task OnSyncingStateChanged_Fires_DuringLoad() => await base.OnSyncingStateChanged_Fires_DuringLoad(); + [TestMethod] + public override void ContextIds_CanBeSetAndRetrieved() => base.ContextIds_CanBeSetAndRetrieved(); + [TestMethod] + public override void FilterText_CanBeSetAndRetrieved() => base.FilterText_CanBeSetAndRetrieved(); + [TestMethod] + public override void IList_Add_ReturnsCorrectIndex() => base.IList_Add_ReturnsCorrectIndex(); + [TestMethod] + public override void IList_Contains_WorksCorrectly() => base.IList_Contains_WorksCorrectly(); + [TestMethod] + public override void IList_IndexOf_WorksCorrectly() => base.IList_IndexOf_WorksCorrectly(); + [TestMethod] + public override void IList_Insert_WorksCorrectly() => base.IList_Insert_WorksCorrectly(); + [TestMethod] + public override void IList_Remove_WorksCorrectly() => base.IList_Remove_WorksCorrectly(); + [TestMethod] + public override void IList_Indexer_GetSet_WorksCorrectly() => base.IList_Indexer_GetSet_WorksCorrectly(); + [TestMethod] + public override void ICollection_CopyTo_WorksCorrectly() => base.ICollection_CopyTo_WorksCorrectly(); + [TestMethod] + public override void IsSynchronized_ReturnsTrue() => base.IsSynchronized_ReturnsTrue(); + [TestMethod] + public override void SyncRoot_IsNotNull() => base.SyncRoot_IsNotNull(); + [TestMethod] + public override void IsFixedSize_ReturnsFalse() => base.IsFixedSize_ReturnsFalse(); + [TestMethod] + public override void IsReadOnly_ReturnsFalse() => base.IsReadOnly_ReturnsFalse(); + [TestMethod] + public override async Task Indexer_OutOfRange_ThrowsException() => await base.Indexer_OutOfRange_ThrowsException(); + [TestMethod] + public override void Add_ThenRemove_ClearsTracking() => base.Add_ThenRemove_ClearsTracking(); + [TestMethod] + public override async Task ComplexWorkflow_AddUpdateRemoveSave() => await base.ComplexWorkflow_AddUpdateRemoveSave(); } \ No newline at end of file diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Binary_NoRef.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Binary_NoRef.cs index a5e379a..0efdd5f 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Binary_NoRef.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Binary_NoRef.cs @@ -11,4 +11,153 @@ public class SignalRDataSourceTests_List_Binary_NoRef : SignalRDataSourceTestBas protected override AcSerializerOptions SerializerOption => new AcBinarySerializerOptions { UseReferenceHandling = false }; protected override TestOrderItemListDataSource CreateDataSource(TestableSignalRClient2 client, SignalRCrudTags crudTags) => new(client, crudTags); + + [TestMethod] + public override async Task LoadDataSource_ReturnsAllItems() => await base.LoadDataSource_ReturnsAllItems(); + [TestMethod] + public override async Task LoadDataSource_ClearsChangeTracking_ByDefault() => await base.LoadDataSource_ClearsChangeTracking_ByDefault(); + [TestMethod] + public override async Task LoadDataSource_PreservesChangeTracking_WhenFalse() => await base.LoadDataSource_PreservesChangeTracking_WhenFalse(); + [TestMethod] + public override async Task LoadDataSource_InvokesOnDataSourceLoaded() => await base.LoadDataSource_InvokesOnDataSourceLoaded(); + [TestMethod] + public override async Task LoadDataSource_MultipleCalls_RefreshesData() => await base.LoadDataSource_MultipleCalls_RefreshesData(); + [TestMethod] + public override async Task LoadDataSourceAsync_LoadsDataViaCallback() => await base.LoadDataSourceAsync_LoadsDataViaCallback(); + [TestMethod] + public override async Task LoadItem_ReturnsSingleItem() => await base.LoadItem_ReturnsSingleItem(); + [TestMethod] + public override async Task LoadItem_AddsToDataSource_WhenNotExists() => await base.LoadItem_AddsToDataSource_WhenNotExists(); + [TestMethod] + public override async Task LoadItem_UpdatesExisting_WhenAlreadyLoaded() => await base.LoadItem_UpdatesExisting_WhenAlreadyLoaded(); + [TestMethod] + public override async Task LoadItem_InvokesOnDataSourceItemChanged() => await base.LoadItem_InvokesOnDataSourceItemChanged(); + [TestMethod] + public override async Task LoadItem_ReturnsNull_WhenNotFound() => await base.LoadItem_ReturnsNull_WhenNotFound(); + [TestMethod] + public override async Task Add_WithAutoSave_AddsItem() => await base.Add_WithAutoSave_AddsItem(); + [TestMethod] + public override void Add_WithoutAutoSave_AddsToTrackingOnly() => base.Add_WithoutAutoSave_AddsToTrackingOnly(); + [TestMethod] + public override void Add_DuplicateId_ThrowsException() => base.Add_DuplicateId_ThrowsException(); + [TestMethod] + public override void Add_DefaultId_ThrowsException() => base.Add_DefaultId_ThrowsException(); + [TestMethod] + public override void AddRange_AddsMultipleItems() => base.AddRange_AddsMultipleItems(); + [TestMethod] + public override async Task AddOrUpdate_AddsNew_WhenNotExists() => await base.AddOrUpdate_AddsNew_WhenNotExists(); + [TestMethod] + public override async Task AddOrUpdate_UpdatesExisting_WhenExists() => await base.AddOrUpdate_UpdatesExisting_WhenExists(); + [TestMethod] + public override void Insert_AtIndex_InsertsCorrectly() => base.Insert_AtIndex_InsertsCorrectly(); + [TestMethod] + public override async Task Insert_WithAutoSave_SavesImmediately() => await base.Insert_WithAutoSave_SavesImmediately(); + [TestMethod] + public override async Task Update_ByIndex_UpdatesCorrectly() => await base.Update_ByIndex_UpdatesCorrectly(); + [TestMethod] + public override async Task Update_ByItem_UpdatesCorrectly() => await base.Update_ByItem_UpdatesCorrectly(); + [TestMethod] + public override void Indexer_Set_TracksUpdate() => base.Indexer_Set_TracksUpdate(); + [TestMethod] + public override async Task Remove_ById_RemovesItem() => await base.Remove_ById_RemovesItem(); + [TestMethod] + public override async Task Remove_ByItem_RemovesItem() => await base.Remove_ByItem_RemovesItem(); + [TestMethod] + public override void Remove_WithoutAutoSave_TracksRemoval() => base.Remove_WithoutAutoSave_TracksRemoval(); + [TestMethod] + public override void RemoveAt_RemovesCorrectItem() => base.RemoveAt_RemovesCorrectItem(); + [TestMethod] + public override async Task RemoveAt_WithAutoSave_SavesImmediately() => await base.RemoveAt_WithAutoSave_SavesImmediately(); + [TestMethod] + public override void TryRemove_ReturnsTrue_WhenExists() => base.TryRemove_ReturnsTrue_WhenExists(); + [TestMethod] + public override void TryRemove_ReturnsFalse_WhenNotExists() => base.TryRemove_ReturnsFalse_WhenNotExists(); + [TestMethod] + public override async Task SaveChanges_SavesTrackedItems() => await base.SaveChanges_SavesTrackedItems(); + [TestMethod] + public override async Task SaveChangesAsync_ClearsTracking() => await base.SaveChangesAsync_ClearsTracking(); + [TestMethod] + public override async Task SaveItem_ById_SavesSpecificItem() => await base.SaveItem_ById_SavesSpecificItem(); + [TestMethod] + public override async Task SaveItem_WithTrackingState_SavesCorrectly() => await base.SaveItem_WithTrackingState_SavesCorrectly(); + [TestMethod] + public override void SetTrackingStateToUpdate_MarksItemForUpdate() => base.SetTrackingStateToUpdate_MarksItemForUpdate(); + [TestMethod] + public override void SetTrackingStateToUpdate_DoesNotChangeAddState() => base.SetTrackingStateToUpdate_DoesNotChangeAddState(); + [TestMethod] + public override void TryGetTrackingItem_ReturnsTrue_WhenTracked() => base.TryGetTrackingItem_ReturnsTrue_WhenTracked(); + [TestMethod] + public override void TryGetTrackingItem_ReturnsFalse_WhenNotTracked() => base.TryGetTrackingItem_ReturnsFalse_WhenNotTracked(); + [TestMethod] + public override void TryRollbackItem_RevertsAddedItem() => base.TryRollbackItem_RevertsAddedItem(); + [TestMethod] + public override async Task TryRollbackItem_RevertsUpdatedItem() => await base.TryRollbackItem_RevertsUpdatedItem(); + [TestMethod] + public override void Rollback_RevertsAllChanges() => base.Rollback_RevertsAllChanges(); + [TestMethod] + public override async Task Count_ReturnsCorrectValue() => await base.Count_ReturnsCorrectValue(); + [TestMethod] + public override void Clear_RemovesAllItems() => base.Clear_RemovesAllItems(); + [TestMethod] + public override void Clear_WithoutClearingTracking_PreservesTracking() => base.Clear_WithoutClearingTracking_PreservesTracking(); + [TestMethod] + public override async Task Contains_ReturnsTrue_WhenItemExists() => await base.Contains_ReturnsTrue_WhenItemExists(); + [TestMethod] + public override void Contains_ReturnsFalse_WhenItemNotExists() => base.Contains_ReturnsFalse_WhenItemNotExists(); + [TestMethod] + public override async Task IndexOf_ReturnsCorrectIndex() => await base.IndexOf_ReturnsCorrectIndex(); + [TestMethod] + public override void IndexOf_ById_ReturnsCorrectIndex() => base.IndexOf_ById_ReturnsCorrectIndex(); + [TestMethod] + public override void TryGetIndex_ReturnsTrue_WhenExists() => base.TryGetIndex_ReturnsTrue_WhenExists(); + [TestMethod] + public override async Task TryGetValue_ReturnsItem_WhenExists() => await base.TryGetValue_ReturnsItem_WhenExists(); + [TestMethod] + public override void TryGetValue_ReturnsFalse_WhenNotExists() => base.TryGetValue_ReturnsFalse_WhenNotExists(); + [TestMethod] + public override async Task CopyTo_CopiesAllItems() => await base.CopyTo_CopiesAllItems(); + [TestMethod] + public override async Task GetEnumerator_EnumeratesAllItems() => await base.GetEnumerator_EnumeratesAllItems(); + [TestMethod] + public override async Task AsReadOnly_ReturnsReadOnlyCollection() => await base.AsReadOnly_ReturnsReadOnlyCollection(); + [TestMethod] + public override async Task SetWorkingReferenceList_SetsNewInnerList() => await base.SetWorkingReferenceList_SetsNewInnerList(); + [TestMethod] + public override async Task GetReferenceInnerList_ReturnsInnerList() => await base.GetReferenceInnerList_ReturnsInnerList(); + [TestMethod] + public override void IsSyncing_IsFalse_Initially() => base.IsSyncing_IsFalse_Initially(); + [TestMethod] + public override async Task OnSyncingStateChanged_Fires_DuringLoad() => await base.OnSyncingStateChanged_Fires_DuringLoad(); + [TestMethod] + public override void ContextIds_CanBeSetAndRetrieved() => base.ContextIds_CanBeSetAndRetrieved(); + [TestMethod] + public override void FilterText_CanBeSetAndRetrieved() => base.FilterText_CanBeSetAndRetrieved(); + [TestMethod] + public override void IList_Add_ReturnsCorrectIndex() => base.IList_Add_ReturnsCorrectIndex(); + [TestMethod] + public override void IList_Contains_WorksCorrectly() => base.IList_Contains_WorksCorrectly(); + [TestMethod] + public override void IList_IndexOf_WorksCorrectly() => base.IList_IndexOf_WorksCorrectly(); + [TestMethod] + public override void IList_Insert_WorksCorrectly() => base.IList_Insert_WorksCorrectly(); + [TestMethod] + public override void IList_Remove_WorksCorrectly() => base.IList_Remove_WorksCorrectly(); + [TestMethod] + public override void IList_Indexer_GetSet_WorksCorrectly() => base.IList_Indexer_GetSet_WorksCorrectly(); + [TestMethod] + public override void ICollection_CopyTo_WorksCorrectly() => base.ICollection_CopyTo_WorksCorrectly(); + [TestMethod] + public override void IsSynchronized_ReturnsTrue() => base.IsSynchronized_ReturnsTrue(); + [TestMethod] + public override void SyncRoot_IsNotNull() => base.SyncRoot_IsNotNull(); + [TestMethod] + public override void IsFixedSize_ReturnsFalse() => base.IsFixedSize_ReturnsFalse(); + [TestMethod] + public override void IsReadOnly_ReturnsFalse() => base.IsReadOnly_ReturnsFalse(); + [TestMethod] + public override async Task Indexer_OutOfRange_ThrowsException() => await base.Indexer_OutOfRange_ThrowsException(); + [TestMethod] + public override void Add_ThenRemove_ClearsTracking() => base.Add_ThenRemove_ClearsTracking(); + [TestMethod] + public override async Task ComplexWorkflow_AddUpdateRemoveSave() => await base.ComplexWorkflow_AddUpdateRemoveSave(); } \ No newline at end of file diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Json.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Json.cs index 77ac197..fbc49a0 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Json.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Json.cs @@ -1,3 +1,4 @@ +using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Jsons; using AyCode.Core.Tests.TestModels; using AyCode.Services.SignalRs; @@ -10,4 +11,153 @@ public class SignalRDataSourceTests_List_Json : SignalRDataSourceTestBase new AcJsonSerializerOptions(); protected override TestOrderItemListDataSource CreateDataSource(TestableSignalRClient2 client, SignalRCrudTags crudTags) => new(client, crudTags); + + [TestMethod] + public override async Task LoadDataSource_ReturnsAllItems() => await base.LoadDataSource_ReturnsAllItems(); + [TestMethod] + public override async Task LoadDataSource_ClearsChangeTracking_ByDefault() => await base.LoadDataSource_ClearsChangeTracking_ByDefault(); + [TestMethod] + public override async Task LoadDataSource_PreservesChangeTracking_WhenFalse() => await base.LoadDataSource_PreservesChangeTracking_WhenFalse(); + [TestMethod] + public override async Task LoadDataSource_InvokesOnDataSourceLoaded() => await base.LoadDataSource_InvokesOnDataSourceLoaded(); + [TestMethod] + public override async Task LoadDataSource_MultipleCalls_RefreshesData() => await base.LoadDataSource_MultipleCalls_RefreshesData(); + [TestMethod] + public override async Task LoadDataSourceAsync_LoadsDataViaCallback() => await base.LoadDataSourceAsync_LoadsDataViaCallback(); + [TestMethod] + public override async Task LoadItem_ReturnsSingleItem() => await base.LoadItem_ReturnsSingleItem(); + [TestMethod] + public override async Task LoadItem_AddsToDataSource_WhenNotExists() => await base.LoadItem_AddsToDataSource_WhenNotExists(); + [TestMethod] + public override async Task LoadItem_UpdatesExisting_WhenAlreadyLoaded() => await base.LoadItem_UpdatesExisting_WhenAlreadyLoaded(); + [TestMethod] + public override async Task LoadItem_InvokesOnDataSourceItemChanged() => await base.LoadItem_InvokesOnDataSourceItemChanged(); + [TestMethod] + public override async Task LoadItem_ReturnsNull_WhenNotFound() => await base.LoadItem_ReturnsNull_WhenNotFound(); + [TestMethod] + public override async Task Add_WithAutoSave_AddsItem() => await base.Add_WithAutoSave_AddsItem(); + [TestMethod] + public override void Add_WithoutAutoSave_AddsToTrackingOnly() => base.Add_WithoutAutoSave_AddsToTrackingOnly(); + [TestMethod] + public override void Add_DuplicateId_ThrowsException() => base.Add_DuplicateId_ThrowsException(); + [TestMethod] + public override void Add_DefaultId_ThrowsException() => base.Add_DefaultId_ThrowsException(); + [TestMethod] + public override void AddRange_AddsMultipleItems() => base.AddRange_AddsMultipleItems(); + [TestMethod] + public override async Task AddOrUpdate_AddsNew_WhenNotExists() => await base.AddOrUpdate_AddsNew_WhenNotExists(); + [TestMethod] + public override async Task AddOrUpdate_UpdatesExisting_WhenExists() => await base.AddOrUpdate_UpdatesExisting_WhenExists(); + [TestMethod] + public override void Insert_AtIndex_InsertsCorrectly() => base.Insert_AtIndex_InsertsCorrectly(); + [TestMethod] + public override async Task Insert_WithAutoSave_SavesImmediately() => await base.Insert_WithAutoSave_SavesImmediately(); + [TestMethod] + public override async Task Update_ByIndex_UpdatesCorrectly() => await base.Update_ByIndex_UpdatesCorrectly(); + [TestMethod] + public override async Task Update_ByItem_UpdatesCorrectly() => await base.Update_ByItem_UpdatesCorrectly(); + [TestMethod] + public override void Indexer_Set_TracksUpdate() => base.Indexer_Set_TracksUpdate(); + [TestMethod] + public override async Task Remove_ById_RemovesItem() => await base.Remove_ById_RemovesItem(); + [TestMethod] + public override async Task Remove_ByItem_RemovesItem() => await base.Remove_ByItem_RemovesItem(); + [TestMethod] + public override void Remove_WithoutAutoSave_TracksRemoval() => base.Remove_WithoutAutoSave_TracksRemoval(); + [TestMethod] + public override void RemoveAt_RemovesCorrectItem() => base.RemoveAt_RemovesCorrectItem(); + [TestMethod] + public override async Task RemoveAt_WithAutoSave_SavesImmediately() => await base.RemoveAt_WithAutoSave_SavesImmediately(); + [TestMethod] + public override void TryRemove_ReturnsTrue_WhenExists() => base.TryRemove_ReturnsTrue_WhenExists(); + [TestMethod] + public override void TryRemove_ReturnsFalse_WhenNotExists() => base.TryRemove_ReturnsFalse_WhenNotExists(); + [TestMethod] + public override async Task SaveChanges_SavesTrackedItems() => await base.SaveChanges_SavesTrackedItems(); + [TestMethod] + public override async Task SaveChangesAsync_ClearsTracking() => await base.SaveChangesAsync_ClearsTracking(); + [TestMethod] + public override async Task SaveItem_ById_SavesSpecificItem() => await base.SaveItem_ById_SavesSpecificItem(); + [TestMethod] + public override async Task SaveItem_WithTrackingState_SavesCorrectly() => await base.SaveItem_WithTrackingState_SavesCorrectly(); + [TestMethod] + public override void SetTrackingStateToUpdate_MarksItemForUpdate() => base.SetTrackingStateToUpdate_MarksItemForUpdate(); + [TestMethod] + public override void SetTrackingStateToUpdate_DoesNotChangeAddState() => base.SetTrackingStateToUpdate_DoesNotChangeAddState(); + [TestMethod] + public override void TryGetTrackingItem_ReturnsTrue_WhenTracked() => base.TryGetTrackingItem_ReturnsTrue_WhenTracked(); + [TestMethod] + public override void TryGetTrackingItem_ReturnsFalse_WhenNotTracked() => base.TryGetTrackingItem_ReturnsFalse_WhenNotTracked(); + [TestMethod] + public override void TryRollbackItem_RevertsAddedItem() => base.TryRollbackItem_RevertsAddedItem(); + [TestMethod] + public override async Task TryRollbackItem_RevertsUpdatedItem() => await base.TryRollbackItem_RevertsUpdatedItem(); + [TestMethod] + public override void Rollback_RevertsAllChanges() => base.Rollback_RevertsAllChanges(); + [TestMethod] + public override async Task Count_ReturnsCorrectValue() => await base.Count_ReturnsCorrectValue(); + [TestMethod] + public override void Clear_RemovesAllItems() => base.Clear_RemovesAllItems(); + [TestMethod] + public override void Clear_WithoutClearingTracking_PreservesTracking() => base.Clear_WithoutClearingTracking_PreservesTracking(); + [TestMethod] + public override async Task Contains_ReturnsTrue_WhenItemExists() => await base.Contains_ReturnsTrue_WhenItemExists(); + [TestMethod] + public override void Contains_ReturnsFalse_WhenItemNotExists() => base.Contains_ReturnsFalse_WhenItemNotExists(); + [TestMethod] + public override async Task IndexOf_ReturnsCorrectIndex() => await base.IndexOf_ReturnsCorrectIndex(); + [TestMethod] + public override void IndexOf_ById_ReturnsCorrectIndex() => base.IndexOf_ById_ReturnsCorrectIndex(); + [TestMethod] + public override void TryGetIndex_ReturnsTrue_WhenExists() => base.TryGetIndex_ReturnsTrue_WhenExists(); + [TestMethod] + public override async Task TryGetValue_ReturnsItem_WhenExists() => await base.TryGetValue_ReturnsItem_WhenExists(); + [TestMethod] + public override void TryGetValue_ReturnsFalse_WhenNotExists() => base.TryGetValue_ReturnsFalse_WhenNotExists(); + [TestMethod] + public override async Task CopyTo_CopiesAllItems() => await base.CopyTo_CopiesAllItems(); + [TestMethod] + public override async Task GetEnumerator_EnumeratesAllItems() => await base.GetEnumerator_EnumeratesAllItems(); + [TestMethod] + public override async Task AsReadOnly_ReturnsReadOnlyCollection() => await base.AsReadOnly_ReturnsReadOnlyCollection(); + [TestMethod] + public override async Task SetWorkingReferenceList_SetsNewInnerList() => await base.SetWorkingReferenceList_SetsNewInnerList(); + [TestMethod] + public override async Task GetReferenceInnerList_ReturnsInnerList() => await base.GetReferenceInnerList_ReturnsInnerList(); + [TestMethod] + public override void IsSyncing_IsFalse_Initially() => base.IsSyncing_IsFalse_Initially(); + [TestMethod] + public override async Task OnSyncingStateChanged_Fires_DuringLoad() => await base.OnSyncingStateChanged_Fires_DuringLoad(); + [TestMethod] + public override void ContextIds_CanBeSetAndRetrieved() => base.ContextIds_CanBeSetAndRetrieved(); + [TestMethod] + public override void FilterText_CanBeSetAndRetrieved() => base.FilterText_CanBeSetAndRetrieved(); + [TestMethod] + public override void IList_Add_ReturnsCorrectIndex() => base.IList_Add_ReturnsCorrectIndex(); + [TestMethod] + public override void IList_Contains_WorksCorrectly() => base.IList_Contains_WorksCorrectly(); + [TestMethod] + public override void IList_IndexOf_WorksCorrectly() => base.IList_IndexOf_WorksCorrectly(); + [TestMethod] + public override void IList_Insert_WorksCorrectly() => base.IList_Insert_WorksCorrectly(); + [TestMethod] + public override void IList_Remove_WorksCorrectly() => base.IList_Remove_WorksCorrectly(); + [TestMethod] + public override void IList_Indexer_GetSet_WorksCorrectly() => base.IList_Indexer_GetSet_WorksCorrectly(); + [TestMethod] + public override void ICollection_CopyTo_WorksCorrectly() => base.ICollection_CopyTo_WorksCorrectly(); + [TestMethod] + public override void IsSynchronized_ReturnsTrue() => base.IsSynchronized_ReturnsTrue(); + [TestMethod] + public override void SyncRoot_IsNotNull() => base.SyncRoot_IsNotNull(); + [TestMethod] + public override void IsFixedSize_ReturnsFalse() => base.IsFixedSize_ReturnsFalse(); + [TestMethod] + public override void IsReadOnly_ReturnsFalse() => base.IsReadOnly_ReturnsFalse(); + [TestMethod] + public override async Task Indexer_OutOfRange_ThrowsException() => await base.Indexer_OutOfRange_ThrowsException(); + [TestMethod] + public override void Add_ThenRemove_ClearsTracking() => base.Add_ThenRemove_ClearsTracking(); + [TestMethod] + public override async Task ComplexWorkflow_AddUpdateRemoveSave() => await base.ComplexWorkflow_AddUpdateRemoveSave(); } \ No newline at end of file diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_Observable_Binary.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_Observable_Binary.cs index af82898..9cec34c 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_Observable_Binary.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_Observable_Binary.cs @@ -12,4 +12,153 @@ public class SignalRDataSourceTests_Observable_Binary : SignalRDataSourceTestBas protected override AcSerializerOptions SerializerOption => new AcBinarySerializerOptions(); protected override TestOrderItemObservableDataSource CreateDataSource(TestableSignalRClient2 client, SignalRCrudTags crudTags) => new(client, crudTags); + + [TestMethod] + public override async Task LoadDataSource_ReturnsAllItems() => await base.LoadDataSource_ReturnsAllItems(); + [TestMethod] + public override async Task LoadDataSource_ClearsChangeTracking_ByDefault() => await base.LoadDataSource_ClearsChangeTracking_ByDefault(); + [TestMethod] + public override async Task LoadDataSource_PreservesChangeTracking_WhenFalse() => await base.LoadDataSource_PreservesChangeTracking_WhenFalse(); + [TestMethod] + public override async Task LoadDataSource_InvokesOnDataSourceLoaded() => await base.LoadDataSource_InvokesOnDataSourceLoaded(); + [TestMethod] + public override async Task LoadDataSource_MultipleCalls_RefreshesData() => await base.LoadDataSource_MultipleCalls_RefreshesData(); + [TestMethod] + public override async Task LoadDataSourceAsync_LoadsDataViaCallback() => await base.LoadDataSourceAsync_LoadsDataViaCallback(); + [TestMethod] + public override async Task LoadItem_ReturnsSingleItem() => await base.LoadItem_ReturnsSingleItem(); + [TestMethod] + public override async Task LoadItem_AddsToDataSource_WhenNotExists() => await base.LoadItem_AddsToDataSource_WhenNotExists(); + [TestMethod] + public override async Task LoadItem_UpdatesExisting_WhenAlreadyLoaded() => await base.LoadItem_UpdatesExisting_WhenAlreadyLoaded(); + [TestMethod] + public override async Task LoadItem_InvokesOnDataSourceItemChanged() => await base.LoadItem_InvokesOnDataSourceItemChanged(); + [TestMethod] + public override async Task LoadItem_ReturnsNull_WhenNotFound() => await base.LoadItem_ReturnsNull_WhenNotFound(); + [TestMethod] + public override async Task Add_WithAutoSave_AddsItem() => await base.Add_WithAutoSave_AddsItem(); + [TestMethod] + public override void Add_WithoutAutoSave_AddsToTrackingOnly() => base.Add_WithoutAutoSave_AddsToTrackingOnly(); + [TestMethod] + public override void Add_DuplicateId_ThrowsException() => base.Add_DuplicateId_ThrowsException(); + [TestMethod] + public override void Add_DefaultId_ThrowsException() => base.Add_DefaultId_ThrowsException(); + [TestMethod] + public override void AddRange_AddsMultipleItems() => base.AddRange_AddsMultipleItems(); + [TestMethod] + public override async Task AddOrUpdate_AddsNew_WhenNotExists() => await base.AddOrUpdate_AddsNew_WhenNotExists(); + [TestMethod] + public override async Task AddOrUpdate_UpdatesExisting_WhenExists() => await base.AddOrUpdate_UpdatesExisting_WhenExists(); + [TestMethod] + public override void Insert_AtIndex_InsertsCorrectly() => base.Insert_AtIndex_InsertsCorrectly(); + [TestMethod] + public override async Task Insert_WithAutoSave_SavesImmediately() => await base.Insert_WithAutoSave_SavesImmediately(); + [TestMethod] + public override async Task Update_ByIndex_UpdatesCorrectly() => await base.Update_ByIndex_UpdatesCorrectly(); + [TestMethod] + public override async Task Update_ByItem_UpdatesCorrectly() => await base.Update_ByItem_UpdatesCorrectly(); + [TestMethod] + public override void Indexer_Set_TracksUpdate() => base.Indexer_Set_TracksUpdate(); + [TestMethod] + public override async Task Remove_ById_RemovesItem() => await base.Remove_ById_RemovesItem(); + [TestMethod] + public override async Task Remove_ByItem_RemovesItem() => await base.Remove_ByItem_RemovesItem(); + [TestMethod] + public override void Remove_WithoutAutoSave_TracksRemoval() => base.Remove_WithoutAutoSave_TracksRemoval(); + [TestMethod] + public override void RemoveAt_RemovesCorrectItem() => base.RemoveAt_RemovesCorrectItem(); + [TestMethod] + public override async Task RemoveAt_WithAutoSave_SavesImmediately() => await base.RemoveAt_WithAutoSave_SavesImmediately(); + [TestMethod] + public override void TryRemove_ReturnsTrue_WhenExists() => base.TryRemove_ReturnsTrue_WhenExists(); + [TestMethod] + public override void TryRemove_ReturnsFalse_WhenNotExists() => base.TryRemove_ReturnsFalse_WhenNotExists(); + [TestMethod] + public override async Task SaveChanges_SavesTrackedItems() => await base.SaveChanges_SavesTrackedItems(); + [TestMethod] + public override async Task SaveChangesAsync_ClearsTracking() => await base.SaveChangesAsync_ClearsTracking(); + [TestMethod] + public override async Task SaveItem_ById_SavesSpecificItem() => await base.SaveItem_ById_SavesSpecificItem(); + [TestMethod] + public override async Task SaveItem_WithTrackingState_SavesCorrectly() => await base.SaveItem_WithTrackingState_SavesCorrectly(); + [TestMethod] + public override void SetTrackingStateToUpdate_MarksItemForUpdate() => base.SetTrackingStateToUpdate_MarksItemForUpdate(); + [TestMethod] + public override void SetTrackingStateToUpdate_DoesNotChangeAddState() => base.SetTrackingStateToUpdate_DoesNotChangeAddState(); + [TestMethod] + public override void TryGetTrackingItem_ReturnsTrue_WhenTracked() => base.TryGetTrackingItem_ReturnsTrue_WhenTracked(); + [TestMethod] + public override void TryGetTrackingItem_ReturnsFalse_WhenNotTracked() => base.TryGetTrackingItem_ReturnsFalse_WhenNotTracked(); + [TestMethod] + public override void TryRollbackItem_RevertsAddedItem() => base.TryRollbackItem_RevertsAddedItem(); + [TestMethod] + public override async Task TryRollbackItem_RevertsUpdatedItem() => await base.TryRollbackItem_RevertsUpdatedItem(); + [TestMethod] + public override void Rollback_RevertsAllChanges() => base.Rollback_RevertsAllChanges(); + [TestMethod] + public override async Task Count_ReturnsCorrectValue() => await base.Count_ReturnsCorrectValue(); + [TestMethod] + public override void Clear_RemovesAllItems() => base.Clear_RemovesAllItems(); + [TestMethod] + public override void Clear_WithoutClearingTracking_PreservesTracking() => base.Clear_WithoutClearingTracking_PreservesTracking(); + [TestMethod] + public override async Task Contains_ReturnsTrue_WhenItemExists() => await base.Contains_ReturnsTrue_WhenItemExists(); + [TestMethod] + public override void Contains_ReturnsFalse_WhenItemNotExists() => base.Contains_ReturnsFalse_WhenItemNotExists(); + [TestMethod] + public override async Task IndexOf_ReturnsCorrectIndex() => await base.IndexOf_ReturnsCorrectIndex(); + [TestMethod] + public override void IndexOf_ById_ReturnsCorrectIndex() => base.IndexOf_ById_ReturnsCorrectIndex(); + [TestMethod] + public override void TryGetIndex_ReturnsTrue_WhenExists() => base.TryGetIndex_ReturnsTrue_WhenExists(); + [TestMethod] + public override async Task TryGetValue_ReturnsItem_WhenExists() => await base.TryGetValue_ReturnsItem_WhenExists(); + [TestMethod] + public override void TryGetValue_ReturnsFalse_WhenNotExists() => base.TryGetValue_ReturnsFalse_WhenNotExists(); + [TestMethod] + public override async Task CopyTo_CopiesAllItems() => await base.CopyTo_CopiesAllItems(); + [TestMethod] + public override async Task GetEnumerator_EnumeratesAllItems() => await base.GetEnumerator_EnumeratesAllItems(); + [TestMethod] + public override async Task AsReadOnly_ReturnsReadOnlyCollection() => await base.AsReadOnly_ReturnsReadOnlyCollection(); + [TestMethod] + public override async Task SetWorkingReferenceList_SetsNewInnerList() => await base.SetWorkingReferenceList_SetsNewInnerList(); + [TestMethod] + public override async Task GetReferenceInnerList_ReturnsInnerList() => await base.GetReferenceInnerList_ReturnsInnerList(); + [TestMethod] + public override void IsSyncing_IsFalse_Initially() => base.IsSyncing_IsFalse_Initially(); + [TestMethod] + public override async Task OnSyncingStateChanged_Fires_DuringLoad() => await base.OnSyncingStateChanged_Fires_DuringLoad(); + [TestMethod] + public override void ContextIds_CanBeSetAndRetrieved() => base.ContextIds_CanBeSetAndRetrieved(); + [TestMethod] + public override void FilterText_CanBeSetAndRetrieved() => base.FilterText_CanBeSetAndRetrieved(); + [TestMethod] + public override void IList_Add_ReturnsCorrectIndex() => base.IList_Add_ReturnsCorrectIndex(); + [TestMethod] + public override void IList_Contains_WorksCorrectly() => base.IList_Contains_WorksCorrectly(); + [TestMethod] + public override void IList_IndexOf_WorksCorrectly() => base.IList_IndexOf_WorksCorrectly(); + [TestMethod] + public override void IList_Insert_WorksCorrectly() => base.IList_Insert_WorksCorrectly(); + [TestMethod] + public override void IList_Remove_WorksCorrectly() => base.IList_Remove_WorksCorrectly(); + [TestMethod] + public override void IList_Indexer_GetSet_WorksCorrectly() => base.IList_Indexer_GetSet_WorksCorrectly(); + [TestMethod] + public override void ICollection_CopyTo_WorksCorrectly() => base.ICollection_CopyTo_WorksCorrectly(); + [TestMethod] + public override void IsSynchronized_ReturnsTrue() => base.IsSynchronized_ReturnsTrue(); + [TestMethod] + public override void SyncRoot_IsNotNull() => base.SyncRoot_IsNotNull(); + [TestMethod] + public override void IsFixedSize_ReturnsFalse() => base.IsFixedSize_ReturnsFalse(); + [TestMethod] + public override void IsReadOnly_ReturnsFalse() => base.IsReadOnly_ReturnsFalse(); + [TestMethod] + public override async Task Indexer_OutOfRange_ThrowsException() => await base.Indexer_OutOfRange_ThrowsException(); + [TestMethod] + public override void Add_ThenRemove_ClearsTracking() => base.Add_ThenRemove_ClearsTracking(); + [TestMethod] + public override async Task ComplexWorkflow_AddUpdateRemoveSave() => await base.ComplexWorkflow_AddUpdateRemoveSave(); } \ No newline at end of file diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_Observable_Json.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_Observable_Json.cs index 74a2b78..c356a2d 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_Observable_Json.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_Observable_Json.cs @@ -11,4 +11,153 @@ public class SignalRDataSourceTests_Observable_Json : SignalRDataSourceTestBase< protected override AcSerializerOptions SerializerOption => new AcJsonSerializerOptions(); protected override TestOrderItemObservableDataSource CreateDataSource(TestableSignalRClient2 client, SignalRCrudTags crudTags) => new(client, crudTags); + + [TestMethod] + public override async Task LoadDataSource_ReturnsAllItems() => await base.LoadDataSource_ReturnsAllItems(); + [TestMethod] + public override async Task LoadDataSource_ClearsChangeTracking_ByDefault() => await base.LoadDataSource_ClearsChangeTracking_ByDefault(); + [TestMethod] + public override async Task LoadDataSource_PreservesChangeTracking_WhenFalse() => await base.LoadDataSource_PreservesChangeTracking_WhenFalse(); + [TestMethod] + public override async Task LoadDataSource_InvokesOnDataSourceLoaded() => await base.LoadDataSource_InvokesOnDataSourceLoaded(); + [TestMethod] + public override async Task LoadDataSource_MultipleCalls_RefreshesData() => await base.LoadDataSource_MultipleCalls_RefreshesData(); + [TestMethod] + public override async Task LoadDataSourceAsync_LoadsDataViaCallback() => await base.LoadDataSourceAsync_LoadsDataViaCallback(); + [TestMethod] + public override async Task LoadItem_ReturnsSingleItem() => await base.LoadItem_ReturnsSingleItem(); + [TestMethod] + public override async Task LoadItem_AddsToDataSource_WhenNotExists() => await base.LoadItem_AddsToDataSource_WhenNotExists(); + [TestMethod] + public override async Task LoadItem_UpdatesExisting_WhenAlreadyLoaded() => await base.LoadItem_UpdatesExisting_WhenAlreadyLoaded(); + [TestMethod] + public override async Task LoadItem_InvokesOnDataSourceItemChanged() => await base.LoadItem_InvokesOnDataSourceItemChanged(); + [TestMethod] + public override async Task LoadItem_ReturnsNull_WhenNotFound() => await base.LoadItem_ReturnsNull_WhenNotFound(); + [TestMethod] + public override async Task Add_WithAutoSave_AddsItem() => await base.Add_WithAutoSave_AddsItem(); + [TestMethod] + public override void Add_WithoutAutoSave_AddsToTrackingOnly() => base.Add_WithoutAutoSave_AddsToTrackingOnly(); + [TestMethod] + public override void Add_DuplicateId_ThrowsException() => base.Add_DuplicateId_ThrowsException(); + [TestMethod] + public override void Add_DefaultId_ThrowsException() => base.Add_DefaultId_ThrowsException(); + [TestMethod] + public override void AddRange_AddsMultipleItems() => base.AddRange_AddsMultipleItems(); + [TestMethod] + public override async Task AddOrUpdate_AddsNew_WhenNotExists() => await base.AddOrUpdate_AddsNew_WhenNotExists(); + [TestMethod] + public override async Task AddOrUpdate_UpdatesExisting_WhenExists() => await base.AddOrUpdate_UpdatesExisting_WhenExists(); + [TestMethod] + public override void Insert_AtIndex_InsertsCorrectly() => base.Insert_AtIndex_InsertsCorrectly(); + [TestMethod] + public override async Task Insert_WithAutoSave_SavesImmediately() => await base.Insert_WithAutoSave_SavesImmediately(); + [TestMethod] + public override async Task Update_ByIndex_UpdatesCorrectly() => await base.Update_ByIndex_UpdatesCorrectly(); + [TestMethod] + public override async Task Update_ByItem_UpdatesCorrectly() => await base.Update_ByItem_UpdatesCorrectly(); + [TestMethod] + public override void Indexer_Set_TracksUpdate() => base.Indexer_Set_TracksUpdate(); + [TestMethod] + public override async Task Remove_ById_RemovesItem() => await base.Remove_ById_RemovesItem(); + [TestMethod] + public override async Task Remove_ByItem_RemovesItem() => await base.Remove_ByItem_RemovesItem(); + [TestMethod] + public override void Remove_WithoutAutoSave_TracksRemoval() => base.Remove_WithoutAutoSave_TracksRemoval(); + [TestMethod] + public override void RemoveAt_RemovesCorrectItem() => base.RemoveAt_RemovesCorrectItem(); + [TestMethod] + public override async Task RemoveAt_WithAutoSave_SavesImmediately() => await base.RemoveAt_WithAutoSave_SavesImmediately(); + [TestMethod] + public override void TryRemove_ReturnsTrue_WhenExists() => base.TryRemove_ReturnsTrue_WhenExists(); + [TestMethod] + public override void TryRemove_ReturnsFalse_WhenNotExists() => base.TryRemove_ReturnsFalse_WhenNotExists(); + [TestMethod] + public override async Task SaveChanges_SavesTrackedItems() => await base.SaveChanges_SavesTrackedItems(); + [TestMethod] + public override async Task SaveChangesAsync_ClearsTracking() => await base.SaveChangesAsync_ClearsTracking(); + [TestMethod] + public override async Task SaveItem_ById_SavesSpecificItem() => await base.SaveItem_ById_SavesSpecificItem(); + [TestMethod] + public override async Task SaveItem_WithTrackingState_SavesCorrectly() => await base.SaveItem_WithTrackingState_SavesCorrectly(); + [TestMethod] + public override void SetTrackingStateToUpdate_MarksItemForUpdate() => base.SetTrackingStateToUpdate_MarksItemForUpdate(); + [TestMethod] + public override void SetTrackingStateToUpdate_DoesNotChangeAddState() => base.SetTrackingStateToUpdate_DoesNotChangeAddState(); + [TestMethod] + public override void TryGetTrackingItem_ReturnsTrue_WhenTracked() => base.TryGetTrackingItem_ReturnsTrue_WhenTracked(); + [TestMethod] + public override void TryGetTrackingItem_ReturnsFalse_WhenNotTracked() => base.TryGetTrackingItem_ReturnsFalse_WhenNotTracked(); + [TestMethod] + public override void TryRollbackItem_RevertsAddedItem() => base.TryRollbackItem_RevertsAddedItem(); + [TestMethod] + public override async Task TryRollbackItem_RevertsUpdatedItem() => await base.TryRollbackItem_RevertsUpdatedItem(); + [TestMethod] + public override void Rollback_RevertsAllChanges() => base.Rollback_RevertsAllChanges(); + [TestMethod] + public override async Task Count_ReturnsCorrectValue() => await base.Count_ReturnsCorrectValue(); + [TestMethod] + public override void Clear_RemovesAllItems() => base.Clear_RemovesAllItems(); + [TestMethod] + public override void Clear_WithoutClearingTracking_PreservesTracking() => base.Clear_WithoutClearingTracking_PreservesTracking(); + [TestMethod] + public override async Task Contains_ReturnsTrue_WhenItemExists() => await base.Contains_ReturnsTrue_WhenItemExists(); + [TestMethod] + public override void Contains_ReturnsFalse_WhenItemNotExists() => base.Contains_ReturnsFalse_WhenItemNotExists(); + [TestMethod] + public override async Task IndexOf_ReturnsCorrectIndex() => await base.IndexOf_ReturnsCorrectIndex(); + [TestMethod] + public override void IndexOf_ById_ReturnsCorrectIndex() => base.IndexOf_ById_ReturnsCorrectIndex(); + [TestMethod] + public override void TryGetIndex_ReturnsTrue_WhenExists() => base.TryGetIndex_ReturnsTrue_WhenExists(); + [TestMethod] + public override async Task TryGetValue_ReturnsItem_WhenExists() => await base.TryGetValue_ReturnsItem_WhenExists(); + [TestMethod] + public override void TryGetValue_ReturnsFalse_WhenNotExists() => base.TryGetValue_ReturnsFalse_WhenNotExists(); + [TestMethod] + public override async Task CopyTo_CopiesAllItems() => await base.CopyTo_CopiesAllItems(); + [TestMethod] + public override async Task GetEnumerator_EnumeratesAllItems() => await base.GetEnumerator_EnumeratesAllItems(); + [TestMethod] + public override async Task AsReadOnly_ReturnsReadOnlyCollection() => await base.AsReadOnly_ReturnsReadOnlyCollection(); + [TestMethod] + public override async Task SetWorkingReferenceList_SetsNewInnerList() => await base.SetWorkingReferenceList_SetsNewInnerList(); + [TestMethod] + public override async Task GetReferenceInnerList_ReturnsInnerList() => await base.GetReferenceInnerList_ReturnsInnerList(); + [TestMethod] + public override void IsSyncing_IsFalse_Initially() => base.IsSyncing_IsFalse_Initially(); + [TestMethod] + public override async Task OnSyncingStateChanged_Fires_DuringLoad() => await base.OnSyncingStateChanged_Fires_DuringLoad(); + [TestMethod] + public override void ContextIds_CanBeSetAndRetrieved() => base.ContextIds_CanBeSetAndRetrieved(); + [TestMethod] + public override void FilterText_CanBeSetAndRetrieved() => base.FilterText_CanBeSetAndRetrieved(); + [TestMethod] + public override void IList_Add_ReturnsCorrectIndex() => base.IList_Add_ReturnsCorrectIndex(); + [TestMethod] + public override void IList_Contains_WorksCorrectly() => base.IList_Contains_WorksCorrectly(); + [TestMethod] + public override void IList_IndexOf_WorksCorrectly() => base.IList_IndexOf_WorksCorrectly(); + [TestMethod] + public override void IList_Insert_WorksCorrectly() => base.IList_Insert_WorksCorrectly(); + [TestMethod] + public override void IList_Remove_WorksCorrectly() => base.IList_Remove_WorksCorrectly(); + [TestMethod] + public override void IList_Indexer_GetSet_WorksCorrectly() => base.IList_Indexer_GetSet_WorksCorrectly(); + [TestMethod] + public override void ICollection_CopyTo_WorksCorrectly() => base.ICollection_CopyTo_WorksCorrectly(); + [TestMethod] + public override void IsSynchronized_ReturnsTrue() => base.IsSynchronized_ReturnsTrue(); + [TestMethod] + public override void SyncRoot_IsNotNull() => base.SyncRoot_IsNotNull(); + [TestMethod] + public override void IsFixedSize_ReturnsFalse() => base.IsFixedSize_ReturnsFalse(); + [TestMethod] + public override void IsReadOnly_ReturnsFalse() => base.IsReadOnly_ReturnsFalse(); + [TestMethod] + public override async Task Indexer_OutOfRange_ThrowsException() => await base.Indexer_OutOfRange_ThrowsException(); + [TestMethod] + public override void Add_ThenRemove_ClearsTracking() => base.Add_ThenRemove_ClearsTracking(); + [TestMethod] + public override async Task ComplexWorkflow_AddUpdateRemoveSave() => await base.ComplexWorkflow_AddUpdateRemoveSave(); } \ No newline at end of file diff --git a/AyCode.Services.Server.Tests/SignalRs/TestSignalRService2.cs b/AyCode.Services.Server.Tests/SignalRs/TestSignalRService2.cs index 1b10b26..a073cf4 100644 --- a/AyCode.Services.Server.Tests/SignalRs/TestSignalRService2.cs +++ b/AyCode.Services.Server.Tests/SignalRs/TestSignalRService2.cs @@ -1,6 +1,8 @@ using System.Globalization; using AyCode.Core.Tests.TestModels; using AyCode.Services.SignalRs; +using static AyCode.Core.Tests.Serialization.AcSerializerModels; +using AyCode.Core.Tests.Serialization; namespace AyCode.Services.Server.Tests.SignalRs; @@ -468,4 +470,72 @@ public class TestSignalRService2 } #endregion + + #region StockTaking Production Bug Reproduction + + /// + /// Simulates the exact production scenario from FruitBank GetStockTakings(false). + /// Returns data from actual database records to reproduce the bug. + /// Uses the REAL StockTaking model from StockTakingTestModels.cs + /// + [SignalR(TestSignalRTags.GetStockTakings)] + public List GetStockTakings(bool loadRelations) + { + // Exact data from production database: + return + [ + new StockTaking + { + Id = 7, + StartDateTime = new DateTime(2025, 12, 3, 8, 55, 43, 539, DateTimeKind.Utc), + IsClosed = false, // This is the key - IsClosed=false gets skipped by serializer! + Creator = 6, + Created = new DateTime(2025, 12, 3, 7, 55, 43, 571, DateTimeKind.Utc), + Modified = new DateTime(2025, 12, 3, 7, 55, 43, 571, DateTimeKind.Utc), + StockTakingItems = loadRelations ? [] : null + }, + new StockTaking + { + Id = 6, + StartDateTime = new DateTime(2025, 12, 2, 8, 21, 26, 439, DateTimeKind.Utc), + IsClosed = true, + Creator = 6, + Created = new DateTime(2025, 12, 2, 7, 21, 26, 468, DateTimeKind.Utc), + Modified = new DateTime(2025, 12, 2, 7, 21, 26, 468, DateTimeKind.Utc), + StockTakingItems = loadRelations ? [] : null + }, + new StockTaking + { + Id = 3, + StartDateTime = new DateTime(2025, 11, 30, 14, 1, 55, 663, DateTimeKind.Utc), + IsClosed = true, + Creator = 6, + Created = new DateTime(2025, 11, 30, 13, 1, 55, 692, DateTimeKind.Utc), + Modified = new DateTime(2025, 11, 30, 13, 1, 55, 692, DateTimeKind.Utc), + StockTakingItems = loadRelations ? [] : null + }, + new StockTaking + { + Id = 2, + StartDateTime = new DateTime(2025, 11, 30, 8, 20, 2, 182, DateTimeKind.Utc), + IsClosed = true, + Creator = 6, + Created = new DateTime(2025, 11, 30, 7, 20, 3, 331, DateTimeKind.Utc), + Modified = new DateTime(2025, 11, 30, 7, 20, 3, 331, DateTimeKind.Utc), + StockTakingItems = loadRelations ? [] : null + }, + new StockTaking + { + Id = 1, + StartDateTime = new DateTime(2025, 11, 30, 8, 18, 59, 693, DateTimeKind.Utc), + IsClosed = true, + Creator = 6, + Created = new DateTime(2025, 11, 30, 7, 19, 1, 849, DateTimeKind.Utc), + Modified = new DateTime(2025, 11, 30, 7, 19, 1, 877, DateTimeKind.Utc), + StockTakingItems = loadRelations ? [] : null + } + ]; + } + + #endregion } diff --git a/AyCode.Services.Server.Tests/SignalRs/TestSignalRTags.cs b/AyCode.Services.Server.Tests/SignalRs/TestSignalRTags.cs index 0b40054..51f0b11 100644 --- a/AyCode.Services.Server.Tests/SignalRs/TestSignalRTags.cs +++ b/AyCode.Services.Server.Tests/SignalRs/TestSignalRTags.cs @@ -88,4 +88,7 @@ public abstract class TestSignalRTags : AcSignalRTags public const int DataSourceAdd = 302; public const int DataSourceUpdate = 303; public const int DataSourceRemove = 304; + + // StockTaking production bug reproduction + public const int GetStockTakings = 400; } diff --git a/AyCode.Services/SignalRs/SignalRSerializationHelper.cs b/AyCode.Services/SignalRs/SignalRSerializationHelper.cs index b050ca6..f3dd3f7 100644 --- a/AyCode.Services/SignalRs/SignalRSerializationHelper.cs +++ b/AyCode.Services/SignalRs/SignalRSerializationHelper.cs @@ -106,7 +106,7 @@ public static class SignalRSerializationHelper public static byte[] SerializeToCompressedJson(T value, AcJsonSerializerOptions? options = null) { var json = value.ToJson(options ?? AcJsonSerializerOptions.Default); - return BrotliHelper.Compress(json); + return GzipHelper.Compress(json); } /// @@ -115,7 +115,7 @@ public static class SignalRSerializationHelper /// public static T? DeserializeFromCompressedJson(byte[] compressedData) { - var (buffer, length) = BrotliHelper.DecompressToRentedBuffer(compressedData.AsSpan()); + var (buffer, length) = GzipHelper.DecompressToRentedBuffer(compressedData.AsSpan()); try { return AcJsonDeserializer.Deserialize(new ReadOnlySpan(buffer, 0, length)); @@ -133,7 +133,7 @@ public static class SignalRSerializationHelper [MethodImpl(MethodImplOptions.AggressiveInlining)] public static (byte[] Buffer, int Length) DecompressToRentedBuffer(byte[] compressedData) { - return BrotliHelper.DecompressToRentedBuffer(compressedData.AsSpan()); + return GzipHelper.DecompressToRentedBuffer(compressedData.AsSpan()); } #endregion @@ -181,7 +181,7 @@ public static class SignalRSerializationHelper json = responseData.ToJson(jsonOptions); } - return BrotliHelper.Compress(json); + return GzipHelper.Compress(json); } #endregion diff --git a/MergeBenchmarksHtmlDropdown.bat b/MergeBenchmarksHtmlDropdown.bat new file mode 100644 index 0000000..817aff7 --- /dev/null +++ b/MergeBenchmarksHtmlDropdown.bat @@ -0,0 +1,7 @@ +@echo off +setlocal +set ROOT=h:\Applications\Aycode\Source\AyCode.Core +set COUNT=5 +powershell -ExecutionPolicy Bypass -File "%ROOT%\MergeBenchmarksHtmlDropdown.ps1" -maxCount %COUNT% +endlocal +pause diff --git a/MergeBenchmarksHtmlDropdown.ps1 b/MergeBenchmarksHtmlDropdown.ps1 new file mode 100644 index 0000000..0b111f6 --- /dev/null +++ b/MergeBenchmarksHtmlDropdown.ps1 @@ -0,0 +1,114 @@ +param( + [int]$maxCount = 5 +) + +$root = "h:\Applications\Aycode\Source\AyCode.Core" +$output = Join-Path $root "AllBenchmarksDropdown.html" + +if (Test-Path $output) { Remove-Item $output } + +$htmlFiles = Get-ChildItem -Path $root -Recurse -Filter "*-report.html" | Sort-Object LastWriteTime -Descending | Select-Object -First $maxCount + +@" + + + + + BenchmarkDotNet Riportok (Dropdown) + + + +

BenchmarkDotNet Riportok

+ + +
+
+
+ + + +"@ | Add-Content -Encoding UTF8 -Path $output + +Write-Host "Összefûzve: $output" \ No newline at end of file diff --git a/RunFullBenchmark.bat b/RunFullBenchmark.bat new file mode 100644 index 0000000..dbccca4 --- /dev/null +++ b/RunFullBenchmark.bat @@ -0,0 +1,6 @@ +dotnet run -c Release --project AyCode.Benchmark + +echo. +pause +endlocal +exit /b %EXITCODE% \ No newline at end of file