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:
parent
b37d873792
commit
b38fd480d8
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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; } = "";
|
||||
|
|
|
|||
|
|
@ -53,14 +53,22 @@ public enum TestUserRole
|
|||
/// Implements IId<int> 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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
Loading…
Reference in New Issue