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
This commit is contained in:
Loretta 2026-01-05 09:44:02 +01:00
parent 65a1d25586
commit fd3487c12b
9 changed files with 101 additions and 24 deletions

View File

@ -1114,7 +1114,7 @@ public sealed class JsonExtensionTests
[TestMethod] [TestMethod]
public void Deserialize_GenericEnum_DirectPath() public void Deserialize_GenericEnum_DirectPath()
{ {
var json = "2"; var json = "20";
var result = AcJsonDeserializer.Deserialize<TestStatus>(json); var result = AcJsonDeserializer.Deserialize<TestStatus>(json);
Assert.AreEqual(TestStatus.Processing, result); Assert.AreEqual(TestStatus.Processing, result);
} }

View File

@ -127,4 +127,59 @@ public class AcBinarySerializerObjectTests
Assert.AreEqual(20, target.Child.Id); Assert.AreEqual(20, target.Child.Id);
Assert.AreEqual("UpdatedChild", target.Child.Name); 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<TestHighReuseDto>();
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<TestHighReuseDto>();
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<TestHighReuseDto>();
Assert.IsNotNull(result);
Assert.AreEqual(id, result.Id, $"Id should be {id} after deserialization");
}
} }

View File

@ -6,7 +6,7 @@ using AyCode.Core.Tests.TestModels;
using MessagePack; using MessagePack;
using MessagePack.Resolvers; using MessagePack.Resolvers;
namespace AyCode.Core.Tests.serialization; namespace AyCode.Core.Tests.Serialization;
[TestClass] [TestClass]
public class QuickBenchmark public class QuickBenchmark

View File

@ -1,3 +1,5 @@
using AyCode.Core.Interfaces;
namespace AyCode.Core.Tests.TestModels; namespace AyCode.Core.Tests.TestModels;
/// <summary> /// <summary>

View File

@ -14,12 +14,12 @@ namespace AyCode.Core.Tests.TestModels;
/// </summary> /// </summary>
public enum TestStatus public enum TestStatus
{ {
Pending = 0, Pending = 5,
Active = 1, Active = 10,
Processing = 2, Processing = 20,
Completed = 3, Completed = 30,
Shipped = 4, Shipped = 40,
OnHold = 5 OnHold = 50
} }
/// <summary> /// <summary>

View File

@ -480,7 +480,7 @@ public static class AcSerializerCommon
/// <summary> /// <summary>
/// Creates a compiled setter for a property using expression trees. /// Creates a compiled setter for a property using expression trees.
/// Handles nullable value types correctly. /// Handles nullable value types correctly, including null values.
/// </summary> /// </summary>
public static Action<object, object?> CreateCompiledSetter(Type declaringType, PropertyInfo prop) public static Action<object, object?> CreateCompiledSetter(Type declaringType, PropertyInfo prop)
{ {
@ -496,9 +496,13 @@ public static class AcSerializerCommon
if (underlyingType != null) 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); 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) else if (propertyType.IsValueType)
{ {

View File

@ -1,4 +1,7 @@
using System;
using System.Collections; using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using AyCode.Core.Helpers; using AyCode.Core.Helpers;
using static AyCode.Core.Helpers.JsonUtilities; using static AyCode.Core.Helpers.JsonUtilities;
@ -639,21 +642,24 @@ public static partial class AcBinaryDeserializer
case PropertyAccessorType.Guid: case PropertyAccessorType.Guid:
propInfo.SetGuid(target, default); propInfo.SetGuid(target, default);
return; return;
case PropertyAccessorType.Enum:
propInfo.SetEnumAsInt32(target, 0);
return;
case PropertyAccessorType.Object: case PropertyAccessorType.Object:
default: default:
// For collections: clear instead of setting to null // For collections: clear instead of setting to null
if (propInfo.IsCollection) //if (propInfo.IsCollection)
{ //{
var collection = propInfo.GetValue(target); // var collection = propInfo.GetValue(target);
if (collection is IList list) // if (collection is IList list)
{ // {
list.Clear(); // list.Clear();
} // }
// If not IList or null, leave it as-is (readonly collection or null already) // // If not IList or null, leave it as-is (readonly collection or null already)
return; // return;
} //}
// For other objects: set to null (strings, nullable types, etc.) // Reference types and nullable value types: set to null
propInfo.SetValue(target, null); propInfo.SetValue(target, null);
return; return;
} }

View File

@ -1002,7 +1002,10 @@ public static partial class AcBinarySerializer
{ {
// Object type - use regular getter // Object type - use regular getter
var value = prop.GetValue(obj); 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); context.WriteByte(BinaryTypeCode.PropertySkip);
} }

View File

@ -215,7 +215,14 @@ internal static class BinaryTypeCode
public const byte Int32TinyMax = 255; // Upper bound for tiny int (192 + 64 - 1 = 255) public const byte Int32TinyMax = 255; // Upper bound for tiny int (192 + 64 - 1 = 255)
// Property skip marker (for single-pass serialization optimization) // 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)
/// <summary> /// <summary>
/// Check if type code represents a reference (string or object). /// Check if type code represents a reference (string or object).