Refactor test data, MessagePack, and serializer logic

- Moved test data creation to BenchmarkTestDataProvider.cs and removed from Program.cs for better organization.
- Added [MessagePackObject]/[Key] attributes to all test models for explicit MessagePack serialization.
- Updated MessagePack benchmark to use MessagePackSerializerOptions.Standard.
- Improved AcBinaryDeserializer string cache with ASCII byte match to prevent hash collision bugs.
- Optimized AcBinarySerializer/Deserializer for string property handling and non-primitive writes.
- Set AcBinarySerializerOptions.UseMetadata default to true for safer deserialization.
This commit is contained in:
Loretta 2026-02-08 10:25:23 +01:00
parent b37d873792
commit b38fd480d8
8 changed files with 410 additions and 262 deletions

View File

@ -0,0 +1,234 @@
using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Tests.TestModels;
namespace AyCode.Core.Serializers.Console;
internal static class BenchmarkTestDataProvider
{
internal static List<TestDataSet> CreateTestDataSets()
{
return new List<TestDataSet>
{
CreateSmallTestData(),
CreateMediumTestData(),
CreateLargeTestData(),
CreateRepeatedStringsTestData(),
CreateDeepNestedTestData()
};
}
internal static TestOrder CreateProfilerOrder()
{
TestDataFactory.ResetIdCounter();
var sharedTag = TestDataFactory.CreateTag("SharedTag");
var sharedUser = TestDataFactory.CreateUser("shareduser");
return TestDataFactory.CreateOrder(
itemCount: 3,
palletsPerItem: 3,
measurementsPerPallet: 3,
pointsPerMeasurement: 4,
sharedTag: sharedTag,
sharedUser: sharedUser);
}
private static TestDataSet CreateSmallTestData()
{
TestDataFactory.ResetIdCounter();
var sharedTag = TestDataFactory.CreateTag("SharedTag");
var sharedUser = TestDataFactory.CreateUser("shareduser");
var order = TestDataFactory.CreateOrder(
itemCount: 2,
palletsPerItem: 2,
measurementsPerPallet: 2,
pointsPerMeasurement: 2,
sharedTag: sharedTag,
sharedUser: sharedUser);
ClearDeepLevelRefs(order);
return new TestDataSet("Small (2x2x2x2)", order, iidRefPercent: 10);
}
private static TestDataSet CreateMediumTestData()
{
TestDataFactory.ResetIdCounter();
var sharedTag = TestDataFactory.CreateTag("SharedTag");
var sharedUser = TestDataFactory.CreateUser("shareduser");
var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true);
var sharedPreferences = new UserPreferences
{
Theme = "dark",
Language = "en-US",
NotificationsEnabled = true,
EmailDigestFrequency = "weekly"
};
sharedUser.Preferences = sharedPreferences;
var order = TestDataFactory.CreateOrder(
itemCount: 3,
palletsPerItem: 3,
measurementsPerPallet: 3,
pointsPerMeasurement: 4,
sharedTag: sharedTag,
sharedUser: sharedUser,
sharedMetadata: sharedMeta,
sharedPreferences: sharedPreferences);
ClearDeepLevelRefs(order);
return new TestDataSet("Medium (3x3x3x4)", order, iidRefPercent: 10);
}
private static TestDataSet CreateLargeTestData()
{
TestDataFactory.ResetIdCounter();
var sharedTag = TestDataFactory.CreateTag("SharedTag");
var sharedUser = TestDataFactory.CreateUser("shareduser");
var sharedPreferences = new UserPreferences
{
Theme = "light",
Language = "de-DE",
NotificationsEnabled = false,
EmailDigestFrequency = "daily"
};
sharedUser.Preferences = sharedPreferences;
var order = TestDataFactory.CreateOrder(
itemCount: 5,
palletsPerItem: 5,
measurementsPerPallet: 5,
pointsPerMeasurement: 10,
sharedTag: sharedTag,
sharedUser: sharedUser,
sharedPreferences: sharedPreferences);
ClearDeepLevelRefs(order);
return new TestDataSet("Large (5x5x5x10)", order, iidRefPercent: 10);
}
private static TestDataSet CreateRepeatedStringsTestData()
{
TestDataFactory.ResetIdCounter();
var sharedTag = TestDataFactory.CreateTag("RepeatedTag");
var sharedUser = TestDataFactory.CreateUser("repeateduser");
var sharedPreferences = new UserPreferences
{
Theme = "dark",
Language = "en-US",
NotificationsEnabled = true,
EmailDigestFrequency = "weekly"
};
sharedUser.Preferences = sharedPreferences;
var order = TestDataFactory.CreateOrder(
itemCount: 10,
palletsPerItem: 2,
measurementsPerPallet: 2,
pointsPerMeasurement: 2,
sharedTag: sharedTag,
sharedUser: sharedUser,
sharedPreferences: sharedPreferences);
foreach (var item in order.Items)
{
item.Status = TestStatus.Processing;
item.ProductName = "CommonProductName_RepeatedForTesting";
}
ClearDeepLevelRefs(order);
return new TestDataSet("Repeated Strings (10 items)", order, iidRefPercent: 10);
}
private static TestDataSet CreateDeepNestedTestData()
{
TestDataFactory.ResetIdCounter();
var sharedTag = TestDataFactory.CreateTag("DeepTag");
var sharedUser = TestDataFactory.CreateUser("deepuser");
var sharedCategory = TestDataFactory.CreateCategory("DeepCategory");
var sharedPreferences = new UserPreferences
{
Theme = "light",
Language = "fr-FR",
NotificationsEnabled = false,
EmailDigestFrequency = "monthly"
};
sharedUser.Preferences = sharedPreferences;
var order = TestDataFactory.CreateOrder(
itemCount: 2,
palletsPerItem: 4,
measurementsPerPallet: 4,
pointsPerMeasurement: 8,
sharedTag: sharedTag,
sharedUser: sharedUser,
sharedPreferences: sharedPreferences,
sharedCategory: sharedCategory);
ClearDeepLevelRefs(order);
return new TestDataSet("Deep Nested (2x4x4x8)", order, iidRefPercent: 10);
}
private static void ClearDeepLevelRefs(TestOrder order)
{
foreach (var item in order.Items)
{
foreach (var pallet in item.Pallets)
{
pallet.Tag = null;
pallet.Inspector = null;
pallet.Category = null;
foreach (var measurement in pallet.Measurements)
{
measurement.Tag = null;
measurement.Operator = null;
foreach (var point in measurement.Points)
{
point.Tag = null;
point.Verifier = null;
}
}
}
}
}
}
internal sealed class TestDataSet
{
public string Name { get; }
public TestOrder Order { get; }
/// <summary>
/// Percentage of IId shared references in the data (0-100).
/// Higher values mean more deduplication benefit for Default mode.
/// </summary>
public int IIdRefPercent { get; }
public TestDataSet(string name, TestOrder order, int iidRefPercent = 0)
{
Name = name;
Order = order;
IIdRefPercent = iidRefPercent;
}
/// <summary>
/// Gets display name including IId ref percentage if set.
/// </summary>
public string DisplayName => IIdRefPercent > 0
? $"{Name} [{IIdRefPercent}% IId refs]"
: Name;
}

View File

@ -68,7 +68,7 @@ public static class Program
}
// Profiler mode: warmup only, then exit (for memory profiler analysis)
//if (mode == "profiler")
if (mode == "profiler")
{
RunProfilerMode();
return;
@ -82,7 +82,7 @@ public static class Program
System.Console.WriteLine();
var allResults = new List<BenchmarkResult>();
var testDataSets = CreateTestDataSets();
var testDataSets = BenchmarkTestDataProvider.CreateTestDataSets();
foreach (var testData in testDataSets)
{
@ -115,17 +115,7 @@ public static class Program
System.Console.WriteLine($"Build: {BuildConfiguration} | .NET: {Environment.Version}");
System.Console.WriteLine();
// Create medium test data
TestDataFactory.ResetIdCounter();
var sharedTag = TestDataFactory.CreateTag("SharedTag");
var sharedUser = TestDataFactory.CreateUser("shareduser");
var order = TestDataFactory.CreateOrder(
itemCount: 3,
palletsPerItem: 3,
measurementsPerPallet: 3,
pointsPerMeasurement: 4,
sharedTag: sharedTag,
sharedUser: sharedUser);
var order = BenchmarkTestDataProvider.CreateProfilerOrder();
var options = AcBinarySerializerOptions.WithoutReferenceHandling;
options.UseStringInterning = StringInterningMode.None;
@ -167,217 +157,6 @@ public static class Program
System.Console.WriteLine("✓ Profiler mode complete. Exiting now.");
}
#region Test Data Creation
private static List<TestDataSet> CreateTestDataSets()
{
return new List<TestDataSet>
{
CreateSmallTestData(),
CreateMediumTestData(),
CreateLargeTestData(),
CreateRepeatedStringsTestData(),
CreateDeepNestedTestData()
};
}
private static TestDataSet CreateSmallTestData()
{
TestDataFactory.ResetIdCounter();
// Create shared references - IId types (only at Order/Item level)
var sharedTag = TestDataFactory.CreateTag("SharedTag");
var sharedUser = TestDataFactory.CreateUser("shareduser");
var order = TestDataFactory.CreateOrder(
itemCount: 2,
palletsPerItem: 2,
measurementsPerPallet: 2,
pointsPerMeasurement: 2,
sharedTag: sharedTag,
sharedUser: sharedUser);
// Clear deeper level refs for realistic ~10% ratio
ClearDeepLevelRefs(order);
return new TestDataSet("Small (2x2x2x2)", order, iidRefPercent: 10);
}
private static TestDataSet CreateMediumTestData()
{
TestDataFactory.ResetIdCounter();
// IId shared references
var sharedTag = TestDataFactory.CreateTag("SharedTag");
var sharedUser = TestDataFactory.CreateUser("shareduser");
var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true);
// Non-IId shared reference - create separate preferences for 2 users
var sharedPreferences = new UserPreferences
{
Theme = "dark",
Language = "en-US",
NotificationsEnabled = true,
EmailDigestFrequency = "weekly"
};
sharedUser.Preferences = sharedPreferences;
var order = TestDataFactory.CreateOrder(
itemCount: 3,
palletsPerItem: 3,
measurementsPerPallet: 3,
pointsPerMeasurement: 4,
sharedTag: sharedTag,
sharedUser: sharedUser,
sharedMetadata: sharedMeta,
sharedPreferences: sharedPreferences);
// Clear deeper level refs for realistic ~10% ratio
ClearDeepLevelRefs(order);
return new TestDataSet("Medium (3x3x3x4)", order, iidRefPercent: 10);
}
private static TestDataSet CreateLargeTestData()
{
TestDataFactory.ResetIdCounter();
// IId shared references
var sharedTag = TestDataFactory.CreateTag("SharedTag");
var sharedUser = TestDataFactory.CreateUser("shareduser");
// Non-IId shared reference
var sharedPreferences = new UserPreferences
{
Theme = "light",
Language = "de-DE",
NotificationsEnabled = false,
EmailDigestFrequency = "daily"
};
sharedUser.Preferences = sharedPreferences;
var order = TestDataFactory.CreateOrder(
itemCount: 5,
palletsPerItem: 5,
measurementsPerPallet: 5,
pointsPerMeasurement: 10,
sharedTag: sharedTag,
sharedUser: sharedUser,
sharedPreferences: sharedPreferences);
// Clear deeper level refs for realistic ~10% ratio
ClearDeepLevelRefs(order);
return new TestDataSet("Large (5x5x5x10)", order, iidRefPercent: 10);
}
private static TestDataSet CreateRepeatedStringsTestData()
{
TestDataFactory.ResetIdCounter();
// IId shared references
var sharedTag = TestDataFactory.CreateTag("RepeatedTag");
var sharedUser = TestDataFactory.CreateUser("repeateduser");
// Non-IId shared reference
var sharedPreferences = new UserPreferences
{
Theme = "dark",
Language = "en-US",
NotificationsEnabled = true,
EmailDigestFrequency = "weekly"
};
sharedUser.Preferences = sharedPreferences;
// Create order with many items to test string interning on repeated property names
var order = TestDataFactory.CreateOrder(
itemCount: 10,
palletsPerItem: 2,
measurementsPerPallet: 2,
pointsPerMeasurement: 2,
sharedTag: sharedTag,
sharedUser: sharedUser,
sharedPreferences: sharedPreferences);
// Set same status and ProductName on all items to test enum and string handling
foreach (var item in order.Items)
{
item.Status = TestStatus.Processing;
item.ProductName = "CommonProductName_RepeatedForTesting";
}
// Clear deeper level refs for realistic ~10% ratio
ClearDeepLevelRefs(order);
return new TestDataSet("Repeated Strings (10 items)", order, iidRefPercent: 10);
}
/// <summary>
/// Clears IId shared references from Pallet, Measurement, and Point levels.
/// This creates a realistic ~10% IId ref ratio (only Order and Item levels have refs).
/// </summary>
private static void ClearDeepLevelRefs(TestOrder order)
{
foreach (var item in order.Items)
{
foreach (var pallet in item.Pallets)
{
pallet.Tag = null;
pallet.Inspector = null;
pallet.Category = null;
foreach (var measurement in pallet.Measurements)
{
measurement.Tag = null;
measurement.Operator = null;
foreach (var point in measurement.Points)
{
point.Tag = null;
point.Verifier = null;
}
}
}
}
}
private static TestDataSet CreateDeepNestedTestData()
{
TestDataFactory.ResetIdCounter();
// IId shared references - only at Order and Item levels for ~10% ratio
var sharedTag = TestDataFactory.CreateTag("DeepTag");
var sharedUser = TestDataFactory.CreateUser("deepuser");
var sharedCategory = TestDataFactory.CreateCategory("DeepCategory");
// Non-IId shared reference
var sharedPreferences = new UserPreferences
{
Theme = "light",
Language = "fr-FR",
NotificationsEnabled = false,
EmailDigestFrequency = "monthly"
};
sharedUser.Preferences = sharedPreferences;
var order = TestDataFactory.CreateOrder(
itemCount: 2,
palletsPerItem: 4,
measurementsPerPallet: 4,
pointsPerMeasurement: 8,
sharedTag: sharedTag,
sharedUser: sharedUser,
sharedPreferences: sharedPreferences,
sharedCategory: sharedCategory);
// Clear deeper level refs for realistic ~10% ratio
ClearDeepLevelRefs(order);
return new TestDataSet("Deep Nested (2x4x4x8)", order, iidRefPercent: 10);
}
#endregion
#region Benchmark Execution
private static List<BenchmarkResult> RunBenchmarksForTestData(TestDataSet testData, string mode)
@ -556,8 +335,11 @@ public static class Program
{
_order = order;
Name = name;
_options = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
//_options = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
//_options = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.Lz4Block);
_options = MessagePackSerializerOptions.Standard.WithCompression(MessagePackCompression.None);
_serialized = MessagePackSerializer.Serialize(order, _options);
}
@ -660,34 +442,6 @@ public static class Program
#region Results
private sealed class TestDataSet
{
public string Name { get; }
public TestOrder Order { get; }
/// <summary>
/// Percentage of IId shared references in the data (0-100).
/// Higher values mean more deduplication benefit for Default mode.
/// </summary>
public int IIdRefPercent { get; }
public TestDataSet(string name, TestOrder order, int iidRefPercent = 0)
{
Name = name;
Order = order;
IIdRefPercent = iidRefPercent;
}
/// <summary>
/// Gets display name including IId ref percentage if set.
/// </summary>
public string DisplayName => IIdRefPercent > 0
? $"{Name} [{IIdRefPercent}% IId refs]"
: Name;
}
private sealed class BenchmarkResult
{
public string TestDataName { get; set; } = "";

View File

@ -53,14 +53,22 @@ public enum TestUserRole
/// Implements IId&lt;int&gt; for semantic $id/$ref serialization.
/// </summary>
[AcBinarySerializable]
[MessagePackObject]
public class SharedTag : IId<int>
{
[Key(0)]
public int Id { get; set; }
[Key(1)]
public string Name { get; set; } = "";
[Key(2)]
public string Color { get; set; } = "#000000";
[Key(3)]
public int Priority { get; set; }
[Key(4)]
public bool IsActive { get; set; } = true;
[Key(5)]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[Key(6)]
public string? Description { get; set; }
}
@ -68,15 +76,24 @@ public class SharedTag : IId<int>
/// Shared category - for hierarchical cross-reference testing.
/// </summary>
[AcBinarySerializable]
[MessagePackObject]
public class SharedCategory : IId<int>
{
[Key(0)]
public int Id { get; set; }
[Key(1)]
public string Name { get; set; } = "";
[Key(2)]
public string? Description { get; set; }
[Key(3)]
public int SortOrder { get; set; }
[Key(4)]
public bool IsDefault { get; set; }
[Key(5)]
public int? ParentCategoryId { get; set; }
[Key(6)]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[Key(7)]
public DateTime? UpdatedAt { get; set; }
}
@ -84,17 +101,28 @@ public class SharedCategory : IId<int>
/// Shared user reference - appears in many places to test $ref deduplication.
/// </summary>
[AcBinarySerializable]
[MessagePackObject]
public class SharedUser : IId<int>
{
[Key(0)]
public int Id { get; set; }
[Key(1)]
public string Username { get; set; } = "";
[Key(2)]
public string Email { get; set; } = "";
[Key(3)]
public string FirstName { get; set; } = "";
[Key(4)]
public string LastName { get; set; } = "";
[Key(5)]
public bool IsActive { get; set; } = true;
[Key(6)]
public TestUserRole Role { get; set; } = TestUserRole.User;
[Key(7)]
public DateTime? LastLoginAt { get; set; }
[Key(8)]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[Key(9)]
public UserPreferences? Preferences { get; set; }
}
@ -102,11 +130,16 @@ public class SharedUser : IId<int>
/// User preferences - non-IId nested object
/// </summary>
[AcBinarySerializable]
[MessagePackObject]
public class UserPreferences
{
[Key(0)]
public string Theme { get; set; } = "light";
[Key(1)]
public string Language { get; set; } = "en-US";
[Key(2)]
public bool NotificationsEnabled { get; set; } = true;
[Key(3)]
public string? EmailDigestFrequency { get; set; }
}
@ -119,15 +152,20 @@ public class UserPreferences
/// Does NOT implement IId, so uses standard Newtonsoft reference tracking.
/// </summary>
[AcBinarySerializable]
[MessagePackObject]
public class MetadataInfo
{
[Key(0)]
public string Key { get; set; } = "";
[Key(1)]
public string Value { get; set; } = "";
[Key(2)]
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
/// <summary>
/// Nested metadata for deep Newtonsoft reference testing
/// </summary>
[Key(3)]
public MetadataInfo? ChildMetadata { get; set; }
}
@ -139,34 +177,51 @@ public class MetadataInfo
/// Level 1: Main order - root of the hierarchy
/// </summary>
[AcBinarySerializable]
[MessagePackObject]
public class TestOrder : IId<int>
{
[Key(0)]
public int Id { get; set; }
[Key(1)]
public string OrderNumber { get; set; } = "";
[Key(2)]
public TestStatus Status { get; set; } = TestStatus.Pending;
[Key(3)]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[Key(4)]
public DateTime? PaidDateUtc { get; set; }
[Key(5)]
public decimal TotalAmount { get; set; }
// Level 2 collection
[Key(6)]
public List<TestOrderItem> Items { get; set; } = [];
// Shared reference properties (for $id/$ref testing)
[Key(7)]
public SharedTag? PrimaryTag { get; set; }
[Key(8)]
public SharedTag? SecondaryTag { get; set; }
[Key(9)]
public SharedUser? Owner { get; set; }
[Key(10)]
public SharedCategory? Category { get; set; }
// Collection of shared references
[Key(11)]
public List<SharedTag> Tags { get; set; } = [];
// Non-IId metadata (for Newtonsoft $ref testing)
[Key(12)]
public MetadataInfo? OrderMetadata { get; set; }
[Key(13)]
public MetadataInfo? AuditMetadata { get; set; }
[Key(14)]
public List<MetadataInfo> MetadataList { get; set; } = [];
// NoMerge collection for testing replace behavior
[JsonNoMergeCollection]
[Key(15)]
public List<TestOrderItem> NoMergeItems { get; set; } = [];
// Parent reference - ignored by all serializers to prevent circular references
@ -180,20 +235,30 @@ public class TestOrder : IId<int>
/// Level 2: Order item with pallets
/// </summary>
[AcBinarySerializable]
[MessagePackObject]
public class TestOrderItem : IId<int>
{
[Key(0)]
public int Id { get; set; }
[Key(1)]
public string ProductName { get; set; } = "";
[Key(2)]
public int Quantity { get; set; }
[Key(3)]
public decimal UnitPrice { get; set; }
[Key(4)]
public TestStatus Status { get; set; } = TestStatus.Pending;
// Level 3 collection
[Key(5)]
public List<TestPallet> Pallets { get; set; } = [];
// Shared references
[Key(6)]
public SharedTag? Tag { get; set; }
[Key(7)]
public SharedUser? Assignee { get; set; }
[Key(8)]
public MetadataInfo? ItemMetadata { get; set; }
// Parent reference - ignored by all serializers to prevent circular references
@ -207,23 +272,34 @@ public class TestOrderItem : IId<int>
/// Level 3: Pallet containing measurements
/// </summary>
[AcBinarySerializable]
[MessagePackObject]
public class TestPallet : IId<int>
{
[Key(0)]
public int Id { get; set; }
[Key(1)]
public string PalletCode { get; set; } = "";
[Key(2)]
public int TrayCount { get; set; }
[Key(3)]
public TestStatus Status { get; set; } = TestStatus.Pending;
[Key(4)]
public double Weight { get; set; }
// Level 4 collection
[Key(5)]
public List<TestMeasurement> Measurements { get; set; } = [];
// Shared IId references for better reference testing
[Key(6)]
public SharedTag? Tag { get; set; }
[Key(7)]
public SharedUser? Inspector { get; set; }
[Key(8)]
public SharedCategory? Category { get; set; }
// Non-IId shared references
[Key(9)]
public MetadataInfo? PalletMetadata { get; set; }
// Parent reference - ignored by all serializers to prevent circular references
@ -237,18 +313,26 @@ public class TestPallet : IId<int>
/// Level 4: Measurement with multiple points
/// </summary>
[AcBinarySerializable]
[MessagePackObject]
public class TestMeasurement : IId<int>
{
[Key(0)]
public int Id { get; set; }
[Key(1)]
public string Name { get; set; } = "";
[Key(2)]
public double TotalWeight { get; set; }
[Key(3)]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// Level 5 collection
[Key(4)]
public List<TestMeasurementPoint> Points { get; set; } = [];
// Shared IId references for better reference testing
[Key(5)]
public SharedTag? Tag { get; set; }
[Key(6)]
public SharedUser? Operator { get; set; }
// Parent reference - ignored by all serializers to prevent circular references
@ -262,15 +346,22 @@ public class TestMeasurement : IId<int>
/// Level 5: Deepest level - measurement point
/// </summary>
[AcBinarySerializable]
[MessagePackObject]
public class TestMeasurementPoint : IId<int>
{
[Key(0)]
public int Id { get; set; }
[Key(1)]
public string Label { get; set; } = "";
[Key(2)]
public double Value { get; set; }
[Key(3)]
public DateTime MeasuredAt { get; set; } = DateTime.UtcNow;
// Shared IId reference for better reference testing (many points share same tag/user)
[Key(4)]
public SharedTag? Tag { get; set; }
[Key(5)]
public SharedUser? Verifier { get; set; }
// Parent reference - ignored by all serializers to prevent circular references

View File

@ -415,14 +415,15 @@ public static partial class AcBinaryDeserializer
if (stringCache.TryGetValue(hash, out var cached))
{
// Hash includes all bytes for short strings, so collision is extremely unlikely
// For longer strings, we still verify length as a sanity check
if (cached.Length == length)
// CRITICAL: Verify actual content matches to prevent hash collision data corruption.
// cached.Length is char count, length is UTF-8 byte count — equal only for ASCII.
// For ASCII strings (common case): byte-by-byte comparison against UTF-8 buffer.
if (cached.Length == length && VerifyAsciiUtf8Match(cached, slice))
{
_position += length;
return cached;
}
// Hash collision with different length - fall through to read new value
// Hash collision or non-ASCII length mismatch - fall through to decode
}
var value = Utf8NoBom.GetString(slice);
@ -431,6 +432,22 @@ public static partial class AcBinaryDeserializer
return value;
}
/// <summary>
/// Verifies that a cached ASCII string matches the UTF-8 bytes exactly.
/// For ASCII, each char's low byte equals the UTF-8 byte.
/// Caller guarantees cached.Length == utf8Bytes.Length (ASCII invariant).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool VerifyAsciiUtf8Match(string cached, ReadOnlySpan<byte> utf8Bytes)
{
for (var i = 0; i < cached.Length; i++)
{
if ((byte)cached[i] != utf8Bytes[i])
return false;
}
return true;
}
/// <summary>
/// Compute hash that includes ALL bytes for short strings to avoid collisions.
///

View File

@ -155,8 +155,7 @@ public static partial class AcBinaryDeserializer
var positionBeforeRead = context.Position;
try
{
// Use typed setters for primitives to avoid boxing
// Skip method call for Object/String/Collection types - they can't use typed setters
// Use typed setters for primitives and strings to avoid ReadValue dispatch
if (propInfo.AccessorType != PropertyAccessorType.Object &&
TryReadAndSetTypedValue(ref context, target, propInfo, peekCode))
continue;

View File

@ -915,6 +915,40 @@ public static partial class AcBinaryDeserializer
return true;
}
break;
case PropertyAccessorType.String:
if (BinaryTypeCode.IsFixStr(peekCode))
{
context.ReadByte();
var length = BinaryTypeCode.DecodeFixStrLength(peekCode);
propInfo.SetValue(target, length == 0 ? string.Empty : context.ReadStringUtf8(length));
return true;
}
if (peekCode == BinaryTypeCode.String)
{
context.ReadByte();
propInfo.SetValue(target, ReadPlainString(ref context));
return true;
}
if (peekCode == BinaryTypeCode.StringEmpty)
{
context.ReadByte();
propInfo.SetValue(target, string.Empty);
return true;
}
if (peekCode == BinaryTypeCode.StringInterned)
{
context.ReadByte();
propInfo.SetValue(target, context.GetInternedString((int)context.ReadVarUInt()));
return true;
}
if (peekCode == BinaryTypeCode.StringInternFirst)
{
context.ReadByte();
propInfo.SetValue(target, ReadAndRegisterInternedString(ref context));
return true;
}
break;
}
return false;

View File

@ -412,6 +412,24 @@ public static partial class AcBinarySerializer
if (TryWritePrimitive(value, type, context))
return;
WriteValueNonPrimitive(value, type, context, depth);
}
/// <summary>
/// Writes a non-primitive value (collection, dictionary, byte[], or complex object).
/// Skips null check and TryWritePrimitive — caller guarantees value is non-null and not a primitive type.
/// Called from WritePropertyOrSkip default case (PropertyAccessorType.Object) and WriteValue fallback.
/// </summary>
private static void WriteValueNonPrimitive(object value, Type type, BinarySerializationContext context, int depth)
{
// Nullable<T> where T is a value type: boxed value may be a primitive.
// Only Nullable<T> can be a value type in the Object accessor path.
if (type.IsValueType)
{
if (TryWritePrimitive(value, value.GetType(), context))
return;
}
if (depth > context.MaxDepth)
{
context.WriteByte(BinaryTypeCode.Null);
@ -1243,7 +1261,8 @@ public static partial class AcBinarySerializer
}
default:
{
// Object type - use regular getter
// Object type (collection, complex object, byte[], dictionary)
// TryWritePrimitive is always false for these — skip it via WriteValueNonPrimitive
var value = prop.GetValue(obj);
// SKIP marker only for null (reference types)
@ -1257,7 +1276,7 @@ public static partial class AcBinarySerializer
#if DEBUG
context.CurrentPropertyPath = $"{prop.DeclaringType.Name}.{prop.Name}";
#endif
WriteValue(value, prop.PropertyType, context, depth);
WriteValueNonPrimitive(value, prop.PropertyType, context, depth);
}
return;
}

View File

@ -82,7 +82,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// allowing the deserializer to match properties by name between different types.
/// Default: false (no overhead)
/// </summary>
public bool UseMetadata { get; init; } = false;
public bool UseMetadata { get; init; } = true;
/// <summary>
/// When true, checks for duplicate property name hashes during serialization (UseMetadata mode).