From fd3487c12bb03a8e7ff0ea5cc017de8ce0baf284 Mon Sep 17 00:00:00 2001 From: Loretta Date: Mon, 5 Jan 2026 09:44:02 +0100 Subject: [PATCH] Update enum values, PropertySkip code, and add int tests - Changed TestStatus enum to use non-sequential values (5, 10, 20, ...) - Updated related tests and deserialization logic for new enum values - Changed PropertySkip marker from 253 to 191 to avoid TinyInt conflicts - PropertySkip now only written for null references, not empty values - Improved handling of skipped enum and nullable properties in deserializer - Enhanced compiled property setter for nullable types - Added comprehensive int serialization tests, including edge cases - Fixed namespace casing and added missing using directive --- AyCode.Core.Tests/JsonExtensionTests.cs | 2 +- .../AcBinarySerializerObjectTests.cs | 55 +++++++++++++++++++ .../Serialization/QuickBenchmark.cs | 2 +- .../TestModels/AcSerializerModels.cs | 2 + .../TestModels/SharedTestModels.cs | 12 ++-- AyCode.Core/Serializers/AcSerializerCommon.cs | 10 +++- .../Binaries/AcBinaryDeserializer.Populate.cs | 28 ++++++---- .../Binaries/AcBinarySerializer.cs | 5 +- .../Binaries/AcBinarySerializerOptions.cs | 9 ++- 9 files changed, 101 insertions(+), 24 deletions(-) diff --git a/AyCode.Core.Tests/JsonExtensionTests.cs b/AyCode.Core.Tests/JsonExtensionTests.cs index cf8212f..0dd62c0 100644 --- a/AyCode.Core.Tests/JsonExtensionTests.cs +++ b/AyCode.Core.Tests/JsonExtensionTests.cs @@ -1114,7 +1114,7 @@ public sealed class JsonExtensionTests [TestMethod] public void Deserialize_GenericEnum_DirectPath() { - var json = "2"; + var json = "20"; var result = AcJsonDeserializer.Deserialize(json); Assert.AreEqual(TestStatus.Processing, result); } diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerObjectTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerObjectTests.cs index f5977ec..59543e9 100644 --- a/AyCode.Core.Tests/Serialization/AcBinarySerializerObjectTests.cs +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerObjectTests.cs @@ -127,4 +127,59 @@ public class AcBinarySerializerObjectTests Assert.AreEqual(20, target.Child.Id); Assert.AreEqual("UpdatedChild", target.Child.Name); } + + [TestMethod] + public void Serialize_SimpleId45_RoundTrip() + { + // Simple test to debug Id=45 serialization issue + var item = new TestHighReuseDto { Id = 45, CategoryCode = "test"}; + + var binary = item.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(45, result.Id, "Id should be 45 after deserialization"); + Assert.AreEqual("test", result.CategoryCode); + } + + [TestMethod] + public void Serialize_SimpleId0_RoundTrip() + { + // Test with Id=0 to see if SKIP marker is used correctly + var item = new TestHighReuseDto { Id = 0 }; + + var binary = item.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(0, result.Id, "Id should be 0 after deserialization"); + } + + [TestMethod] + [DataRow(-16, DisplayName = "TinyInt min: -16")] + [DataRow(-1, DisplayName = "Negative: -1")] + [DataRow(0, DisplayName = "Zero: 0")] + [DataRow(1, DisplayName = "Small: 1")] + [DataRow(45, DisplayName = "Was buggy: 45")] + [DataRow(47, DisplayName = "TinyInt max: 47")] + [DataRow(48, DisplayName = "Above TinyInt: 48")] + [DataRow(66, DisplayName = "Was PropertySkip v2: 66")] + [DataRow(100, DisplayName = "Medium: 100")] + [DataRow(191, DisplayName = "Current PropertySkip value: 191")] + [DataRow(192, DisplayName = "TinyInt code start: 192")] + [DataRow(253, DisplayName = "Was PropertySkip v1: 253")] + [DataRow(255, DisplayName = "Max byte: 255")] + [DataRow(1000, DisplayName = "Large: 1000")] + [DataRow(int.MaxValue, DisplayName = "MaxValue")] + [DataRow(int.MinValue, DisplayName = "MinValue")] + public void Serialize_VariousIntIds_PropertySkipTest(int id) + { + var item = new TestHighReuseDto { Id = id, CategoryCode = "test" }; + + var binary = item.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(id, result.Id, $"Id should be {id} after deserialization"); + } } diff --git a/AyCode.Core.Tests/Serialization/QuickBenchmark.cs b/AyCode.Core.Tests/Serialization/QuickBenchmark.cs index b0f51b6..9aaf3fc 100644 --- a/AyCode.Core.Tests/Serialization/QuickBenchmark.cs +++ b/AyCode.Core.Tests/Serialization/QuickBenchmark.cs @@ -6,7 +6,7 @@ using AyCode.Core.Tests.TestModels; using MessagePack; using MessagePack.Resolvers; -namespace AyCode.Core.Tests.serialization; +namespace AyCode.Core.Tests.Serialization; [TestClass] public class QuickBenchmark diff --git a/AyCode.Core.Tests/TestModels/AcSerializerModels.cs b/AyCode.Core.Tests/TestModels/AcSerializerModels.cs index 77b2512..68b9040 100644 --- a/AyCode.Core.Tests/TestModels/AcSerializerModels.cs +++ b/AyCode.Core.Tests/TestModels/AcSerializerModels.cs @@ -1,3 +1,5 @@ +using AyCode.Core.Interfaces; + namespace AyCode.Core.Tests.TestModels; /// diff --git a/AyCode.Core.Tests/TestModels/SharedTestModels.cs b/AyCode.Core.Tests/TestModels/SharedTestModels.cs index 3c4f9a5..60949a1 100644 --- a/AyCode.Core.Tests/TestModels/SharedTestModels.cs +++ b/AyCode.Core.Tests/TestModels/SharedTestModels.cs @@ -14,12 +14,12 @@ namespace AyCode.Core.Tests.TestModels; /// public enum TestStatus { - Pending = 0, - Active = 1, - Processing = 2, - Completed = 3, - Shipped = 4, - OnHold = 5 + Pending = 5, + Active = 10, + Processing = 20, + Completed = 30, + Shipped = 40, + OnHold = 50 } /// diff --git a/AyCode.Core/Serializers/AcSerializerCommon.cs b/AyCode.Core/Serializers/AcSerializerCommon.cs index 2720a24..9d64407 100644 --- a/AyCode.Core/Serializers/AcSerializerCommon.cs +++ b/AyCode.Core/Serializers/AcSerializerCommon.cs @@ -480,7 +480,7 @@ public static class AcSerializerCommon /// /// Creates a compiled setter for a property using expression trees. - /// Handles nullable value types correctly. + /// Handles nullable value types correctly, including null values. /// public static Action CreateCompiledSetter(Type declaringType, PropertyInfo prop) { @@ -496,9 +496,13 @@ public static class AcSerializerCommon if (underlyingType != null) { - // Nullable value type: unbox to underlying type, then convert to nullable + // Nullable value type: handle null case with conditional + // if (value == null) property = default; else property = (T?)Unbox(value) + var nullCheck = LExpression.Equal(valueParam, LExpression.Constant(null, typeof(object))); + var nullValue = LExpression.Default(propertyType); var unboxed = LExpression.Unbox(valueParam, underlyingType); - castValue = LExpression.Convert(unboxed, propertyType); + var converted = LExpression.Convert(unboxed, propertyType); + castValue = LExpression.Condition(nullCheck, nullValue, converted); } else if (propertyType.IsValueType) { diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs index bdb7a1b..22e3bed 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs @@ -1,4 +1,7 @@ +using System; using System.Collections; +using System.Collections.Generic; +using System.Linq; using System.Runtime.CompilerServices; using AyCode.Core.Helpers; using static AyCode.Core.Helpers.JsonUtilities; @@ -639,21 +642,24 @@ public static partial class AcBinaryDeserializer case PropertyAccessorType.Guid: propInfo.SetGuid(target, default); return; + case PropertyAccessorType.Enum: + propInfo.SetEnumAsInt32(target, 0); + return; case PropertyAccessorType.Object: default: // For collections: clear instead of setting to null - if (propInfo.IsCollection) - { - var collection = propInfo.GetValue(target); - if (collection is IList list) - { - list.Clear(); - } - // If not IList or null, leave it as-is (readonly collection or null already) - return; - } + //if (propInfo.IsCollection) + //{ + // var collection = propInfo.GetValue(target); + // if (collection is IList list) + // { + // list.Clear(); + // } + // // If not IList or null, leave it as-is (readonly collection or null already) + // return; + //} - // For other objects: set to null (strings, nullable types, etc.) + // Reference types and nullable value types: set to null propInfo.SetValue(target, null); return; } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index b23fe23..ab1a0a9 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -1002,7 +1002,10 @@ public static partial class AcBinarySerializer { // Object type - use regular getter var value = prop.GetValue(obj); - if (value == null || (prop.PropertyTypeCode == TypeCode.String && string.IsNullOrEmpty((string)value))) + + // SKIP marker only for null (reference types) + // Empty string, empty collections, etc. are valid values and must be written! + if (value == null) { context.WriteByte(BinaryTypeCode.PropertySkip); } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs index c2fa725..0961010 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs @@ -215,7 +215,14 @@ internal static class BinaryTypeCode public const byte Int32TinyMax = 255; // Upper bound for tiny int (192 + 64 - 1 = 255) // Property skip marker (for single-pass serialization optimization) - public const byte PropertySkip = 253; // Marks a property with default/null value (skipped during serialization) + // CRITICAL: Must be in the "reserved" range 67-191 (after FixStr, before TinyInt) + // AND must not conflict with any other type codes. + // Using 191 (0xBF) - the highest value before TinyInt range starts at 192. + // This ensures it won't be confused with: + // - Primitive types (0-31) + // - FixStr (34-65) + // - TinyInt values (192-255) + public const byte PropertySkip = 191; // Marks a property with default/null value (skipped during serialization) /// /// Check if type code represents a reference (string or object).