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) // Profiler mode: warmup only, then exit (for memory profiler analysis)
//if (mode == "profiler") if (mode == "profiler")
{ {
RunProfilerMode(); RunProfilerMode();
return; return;
@ -82,7 +82,7 @@ public static class Program
System.Console.WriteLine(); System.Console.WriteLine();
var allResults = new List<BenchmarkResult>(); var allResults = new List<BenchmarkResult>();
var testDataSets = CreateTestDataSets(); var testDataSets = BenchmarkTestDataProvider.CreateTestDataSets();
foreach (var testData in testDataSets) foreach (var testData in testDataSets)
{ {
@ -115,17 +115,7 @@ public static class Program
System.Console.WriteLine($"Build: {BuildConfiguration} | .NET: {Environment.Version}"); System.Console.WriteLine($"Build: {BuildConfiguration} | .NET: {Environment.Version}");
System.Console.WriteLine(); System.Console.WriteLine();
// Create medium test data var order = BenchmarkTestDataProvider.CreateProfilerOrder();
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 options = AcBinarySerializerOptions.WithoutReferenceHandling; var options = AcBinarySerializerOptions.WithoutReferenceHandling;
options.UseStringInterning = StringInterningMode.None; options.UseStringInterning = StringInterningMode.None;
@ -167,217 +157,6 @@ public static class Program
System.Console.WriteLine("✓ Profiler mode complete. Exiting now."); 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 #region Benchmark Execution
private static List<BenchmarkResult> RunBenchmarksForTestData(TestDataSet testData, string mode) private static List<BenchmarkResult> RunBenchmarksForTestData(TestDataSet testData, string mode)
@ -556,8 +335,11 @@ public static class Program
{ {
_order = order; _order = order;
Name = name; Name = name;
_options = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
//_options = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
//_options = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.Lz4Block); //_options = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.Lz4Block);
_options = MessagePackSerializerOptions.Standard.WithCompression(MessagePackCompression.None);
_serialized = MessagePackSerializer.Serialize(order, _options); _serialized = MessagePackSerializer.Serialize(order, _options);
} }
@ -660,34 +442,6 @@ public static class Program
#region Results #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 private sealed class BenchmarkResult
{ {
public string TestDataName { get; set; } = ""; public string TestDataName { get; set; } = "";

View File

@ -53,14 +53,22 @@ public enum TestUserRole
/// Implements IId&lt;int&gt; for semantic $id/$ref serialization. /// Implements IId&lt;int&gt; for semantic $id/$ref serialization.
/// </summary> /// </summary>
[AcBinarySerializable] [AcBinarySerializable]
[MessagePackObject]
public class SharedTag : IId<int> public class SharedTag : IId<int>
{ {
[Key(0)]
public int Id { get; set; } public int Id { get; set; }
[Key(1)]
public string Name { get; set; } = ""; public string Name { get; set; } = "";
[Key(2)]
public string Color { get; set; } = "#000000"; public string Color { get; set; } = "#000000";
[Key(3)]
public int Priority { get; set; } public int Priority { get; set; }
[Key(4)]
public bool IsActive { get; set; } = true; public bool IsActive { get; set; } = true;
[Key(5)]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[Key(6)]
public string? Description { get; set; } public string? Description { get; set; }
} }
@ -68,15 +76,24 @@ public class SharedTag : IId<int>
/// Shared category - for hierarchical cross-reference testing. /// Shared category - for hierarchical cross-reference testing.
/// </summary> /// </summary>
[AcBinarySerializable] [AcBinarySerializable]
[MessagePackObject]
public class SharedCategory : IId<int> public class SharedCategory : IId<int>
{ {
[Key(0)]
public int Id { get; set; } public int Id { get; set; }
[Key(1)]
public string Name { get; set; } = ""; public string Name { get; set; } = "";
[Key(2)]
public string? Description { get; set; } public string? Description { get; set; }
[Key(3)]
public int SortOrder { get; set; } public int SortOrder { get; set; }
[Key(4)]
public bool IsDefault { get; set; } public bool IsDefault { get; set; }
[Key(5)]
public int? ParentCategoryId { get; set; } public int? ParentCategoryId { get; set; }
[Key(6)]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[Key(7)]
public DateTime? UpdatedAt { get; set; } 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. /// Shared user reference - appears in many places to test $ref deduplication.
/// </summary> /// </summary>
[AcBinarySerializable] [AcBinarySerializable]
[MessagePackObject]
public class SharedUser : IId<int> public class SharedUser : IId<int>
{ {
[Key(0)]
public int Id { get; set; } public int Id { get; set; }
[Key(1)]
public string Username { get; set; } = ""; public string Username { get; set; } = "";
[Key(2)]
public string Email { get; set; } = ""; public string Email { get; set; } = "";
[Key(3)]
public string FirstName { get; set; } = ""; public string FirstName { get; set; } = "";
[Key(4)]
public string LastName { get; set; } = ""; public string LastName { get; set; } = "";
[Key(5)]
public bool IsActive { get; set; } = true; public bool IsActive { get; set; } = true;
[Key(6)]
public TestUserRole Role { get; set; } = TestUserRole.User; public TestUserRole Role { get; set; } = TestUserRole.User;
[Key(7)]
public DateTime? LastLoginAt { get; set; } public DateTime? LastLoginAt { get; set; }
[Key(8)]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[Key(9)]
public UserPreferences? Preferences { get; set; } public UserPreferences? Preferences { get; set; }
} }
@ -102,11 +130,16 @@ public class SharedUser : IId<int>
/// User preferences - non-IId nested object /// User preferences - non-IId nested object
/// </summary> /// </summary>
[AcBinarySerializable] [AcBinarySerializable]
[MessagePackObject]
public class UserPreferences public class UserPreferences
{ {
[Key(0)]
public string Theme { get; set; } = "light"; public string Theme { get; set; } = "light";
[Key(1)]
public string Language { get; set; } = "en-US"; public string Language { get; set; } = "en-US";
[Key(2)]
public bool NotificationsEnabled { get; set; } = true; public bool NotificationsEnabled { get; set; } = true;
[Key(3)]
public string? EmailDigestFrequency { get; set; } public string? EmailDigestFrequency { get; set; }
} }
@ -119,15 +152,20 @@ public class UserPreferences
/// Does NOT implement IId, so uses standard Newtonsoft reference tracking. /// Does NOT implement IId, so uses standard Newtonsoft reference tracking.
/// </summary> /// </summary>
[AcBinarySerializable] [AcBinarySerializable]
[MessagePackObject]
public class MetadataInfo public class MetadataInfo
{ {
[Key(0)]
public string Key { get; set; } = ""; public string Key { get; set; } = "";
[Key(1)]
public string Value { get; set; } = ""; public string Value { get; set; } = "";
[Key(2)]
public DateTime Timestamp { get; set; } = DateTime.UtcNow; public DateTime Timestamp { get; set; } = DateTime.UtcNow;
/// <summary> /// <summary>
/// Nested metadata for deep Newtonsoft reference testing /// Nested metadata for deep Newtonsoft reference testing
/// </summary> /// </summary>
[Key(3)]
public MetadataInfo? ChildMetadata { get; set; } public MetadataInfo? ChildMetadata { get; set; }
} }
@ -139,34 +177,51 @@ public class MetadataInfo
/// Level 1: Main order - root of the hierarchy /// Level 1: Main order - root of the hierarchy
/// </summary> /// </summary>
[AcBinarySerializable] [AcBinarySerializable]
[MessagePackObject]
public class TestOrder : IId<int> public class TestOrder : IId<int>
{ {
[Key(0)]
public int Id { get; set; } public int Id { get; set; }
[Key(1)]
public string OrderNumber { get; set; } = ""; public string OrderNumber { get; set; } = "";
[Key(2)]
public TestStatus Status { get; set; } = TestStatus.Pending; public TestStatus Status { get; set; } = TestStatus.Pending;
[Key(3)]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[Key(4)]
public DateTime? PaidDateUtc { get; set; } public DateTime? PaidDateUtc { get; set; }
[Key(5)]
public decimal TotalAmount { get; set; } public decimal TotalAmount { get; set; }
// Level 2 collection // Level 2 collection
[Key(6)]
public List<TestOrderItem> Items { get; set; } = []; public List<TestOrderItem> Items { get; set; } = [];
// Shared reference properties (for $id/$ref testing) // Shared reference properties (for $id/$ref testing)
[Key(7)]
public SharedTag? PrimaryTag { get; set; } public SharedTag? PrimaryTag { get; set; }
[Key(8)]
public SharedTag? SecondaryTag { get; set; } public SharedTag? SecondaryTag { get; set; }
[Key(9)]
public SharedUser? Owner { get; set; } public SharedUser? Owner { get; set; }
[Key(10)]
public SharedCategory? Category { get; set; } public SharedCategory? Category { get; set; }
// Collection of shared references // Collection of shared references
[Key(11)]
public List<SharedTag> Tags { get; set; } = []; public List<SharedTag> Tags { get; set; } = [];
// Non-IId metadata (for Newtonsoft $ref testing) // Non-IId metadata (for Newtonsoft $ref testing)
[Key(12)]
public MetadataInfo? OrderMetadata { get; set; } public MetadataInfo? OrderMetadata { get; set; }
[Key(13)]
public MetadataInfo? AuditMetadata { get; set; } public MetadataInfo? AuditMetadata { get; set; }
[Key(14)]
public List<MetadataInfo> MetadataList { get; set; } = []; public List<MetadataInfo> MetadataList { get; set; } = [];
// NoMerge collection for testing replace behavior // NoMerge collection for testing replace behavior
[JsonNoMergeCollection] [JsonNoMergeCollection]
[Key(15)]
public List<TestOrderItem> NoMergeItems { get; set; } = []; public List<TestOrderItem> NoMergeItems { get; set; } = [];
// Parent reference - ignored by all serializers to prevent circular references // 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 /// Level 2: Order item with pallets
/// </summary> /// </summary>
[AcBinarySerializable] [AcBinarySerializable]
[MessagePackObject]
public class TestOrderItem : IId<int> public class TestOrderItem : IId<int>
{ {
[Key(0)]
public int Id { get; set; } public int Id { get; set; }
[Key(1)]
public string ProductName { get; set; } = ""; public string ProductName { get; set; } = "";
[Key(2)]
public int Quantity { get; set; } public int Quantity { get; set; }
[Key(3)]
public decimal UnitPrice { get; set; } public decimal UnitPrice { get; set; }
[Key(4)]
public TestStatus Status { get; set; } = TestStatus.Pending; public TestStatus Status { get; set; } = TestStatus.Pending;
// Level 3 collection // Level 3 collection
[Key(5)]
public List<TestPallet> Pallets { get; set; } = []; public List<TestPallet> Pallets { get; set; } = [];
// Shared references // Shared references
[Key(6)]
public SharedTag? Tag { get; set; } public SharedTag? Tag { get; set; }
[Key(7)]
public SharedUser? Assignee { get; set; } public SharedUser? Assignee { get; set; }
[Key(8)]
public MetadataInfo? ItemMetadata { get; set; } public MetadataInfo? ItemMetadata { get; set; }
// Parent reference - ignored by all serializers to prevent circular references // Parent reference - ignored by all serializers to prevent circular references
@ -207,23 +272,34 @@ public class TestOrderItem : IId<int>
/// Level 3: Pallet containing measurements /// Level 3: Pallet containing measurements
/// </summary> /// </summary>
[AcBinarySerializable] [AcBinarySerializable]
[MessagePackObject]
public class TestPallet : IId<int> public class TestPallet : IId<int>
{ {
[Key(0)]
public int Id { get; set; } public int Id { get; set; }
[Key(1)]
public string PalletCode { get; set; } = ""; public string PalletCode { get; set; } = "";
[Key(2)]
public int TrayCount { get; set; } public int TrayCount { get; set; }
[Key(3)]
public TestStatus Status { get; set; } = TestStatus.Pending; public TestStatus Status { get; set; } = TestStatus.Pending;
[Key(4)]
public double Weight { get; set; } public double Weight { get; set; }
// Level 4 collection // Level 4 collection
[Key(5)]
public List<TestMeasurement> Measurements { get; set; } = []; public List<TestMeasurement> Measurements { get; set; } = [];
// Shared IId references for better reference testing // Shared IId references for better reference testing
[Key(6)]
public SharedTag? Tag { get; set; } public SharedTag? Tag { get; set; }
[Key(7)]
public SharedUser? Inspector { get; set; } public SharedUser? Inspector { get; set; }
[Key(8)]
public SharedCategory? Category { get; set; } public SharedCategory? Category { get; set; }
// Non-IId shared references // Non-IId shared references
[Key(9)]
public MetadataInfo? PalletMetadata { get; set; } public MetadataInfo? PalletMetadata { get; set; }
// Parent reference - ignored by all serializers to prevent circular references // 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 /// Level 4: Measurement with multiple points
/// </summary> /// </summary>
[AcBinarySerializable] [AcBinarySerializable]
[MessagePackObject]
public class TestMeasurement : IId<int> public class TestMeasurement : IId<int>
{ {
[Key(0)]
public int Id { get; set; } public int Id { get; set; }
[Key(1)]
public string Name { get; set; } = ""; public string Name { get; set; } = "";
[Key(2)]
public double TotalWeight { get; set; } public double TotalWeight { get; set; }
[Key(3)]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// Level 5 collection // Level 5 collection
[Key(4)]
public List<TestMeasurementPoint> Points { get; set; } = []; public List<TestMeasurementPoint> Points { get; set; } = [];
// Shared IId references for better reference testing // Shared IId references for better reference testing
[Key(5)]
public SharedTag? Tag { get; set; } public SharedTag? Tag { get; set; }
[Key(6)]
public SharedUser? Operator { get; set; } public SharedUser? Operator { get; set; }
// Parent reference - ignored by all serializers to prevent circular references // 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 /// Level 5: Deepest level - measurement point
/// </summary> /// </summary>
[AcBinarySerializable] [AcBinarySerializable]
[MessagePackObject]
public class TestMeasurementPoint : IId<int> public class TestMeasurementPoint : IId<int>
{ {
[Key(0)]
public int Id { get; set; } public int Id { get; set; }
[Key(1)]
public string Label { get; set; } = ""; public string Label { get; set; } = "";
[Key(2)]
public double Value { get; set; } public double Value { get; set; }
[Key(3)]
public DateTime MeasuredAt { get; set; } = DateTime.UtcNow; public DateTime MeasuredAt { get; set; } = DateTime.UtcNow;
// Shared IId reference for better reference testing (many points share same tag/user) // Shared IId reference for better reference testing (many points share same tag/user)
[Key(4)]
public SharedTag? Tag { get; set; } public SharedTag? Tag { get; set; }
[Key(5)]
public SharedUser? Verifier { get; set; } public SharedUser? Verifier { get; set; }
// Parent reference - ignored by all serializers to prevent circular references // 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)) if (stringCache.TryGetValue(hash, out var cached))
{ {
// Hash includes all bytes for short strings, so collision is extremely unlikely // CRITICAL: Verify actual content matches to prevent hash collision data corruption.
// For longer strings, we still verify length as a sanity check // cached.Length is char count, length is UTF-8 byte count — equal only for ASCII.
if (cached.Length == length) // For ASCII strings (common case): byte-by-byte comparison against UTF-8 buffer.
if (cached.Length == length && VerifyAsciiUtf8Match(cached, slice))
{ {
_position += length; _position += length;
return cached; 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); var value = Utf8NoBom.GetString(slice);
@ -431,6 +432,22 @@ public static partial class AcBinaryDeserializer
return value; 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> /// <summary>
/// Compute hash that includes ALL bytes for short strings to avoid collisions. /// 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; var positionBeforeRead = context.Position;
try try
{ {
// Use typed setters for primitives to avoid boxing // Use typed setters for primitives and strings to avoid ReadValue dispatch
// Skip method call for Object/String/Collection types - they can't use typed setters
if (propInfo.AccessorType != PropertyAccessorType.Object && if (propInfo.AccessorType != PropertyAccessorType.Object &&
TryReadAndSetTypedValue(ref context, target, propInfo, peekCode)) TryReadAndSetTypedValue(ref context, target, propInfo, peekCode))
continue; continue;

View File

@ -915,6 +915,40 @@ public static partial class AcBinaryDeserializer
return true; return true;
} }
break; 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; return false;

View File

@ -412,6 +412,24 @@ public static partial class AcBinarySerializer
if (TryWritePrimitive(value, type, context)) if (TryWritePrimitive(value, type, context))
return; 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) if (depth > context.MaxDepth)
{ {
context.WriteByte(BinaryTypeCode.Null); context.WriteByte(BinaryTypeCode.Null);
@ -1243,7 +1261,8 @@ public static partial class AcBinarySerializer
} }
default: 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); var value = prop.GetValue(obj);
// SKIP marker only for null (reference types) // SKIP marker only for null (reference types)
@ -1257,7 +1276,7 @@ public static partial class AcBinarySerializer
#if DEBUG #if DEBUG
context.CurrentPropertyPath = $"{prop.DeclaringType.Name}.{prop.Name}"; context.CurrentPropertyPath = $"{prop.DeclaringType.Name}.{prop.Name}";
#endif #endif
WriteValue(value, prop.PropertyType, context, depth); WriteValueNonPrimitive(value, prop.PropertyType, context, depth);
} }
return; return;
} }

View File

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