Refactor serializer options, string fast paths & analysis

- Refactor all serializer options to use properties returning new instances (no shared mutable state); update all usages accordingly
- Extract AcSerializerOptions, BinaryTypeCode, and BinaryPropertyFilterContext to dedicated files for clarity and reuse
- Add DEBUG-only string interning analysis/reporting tools to AcBinarySerializer
- Improve AcBinarySerializer string property serialization with direct typed getter and SIMD-optimized ASCII path
- Increase benchmark/test warmup iterations and add JIT warmup delays for more reliable performance measurements
- Remove redundant usings and update documentation/comments throughout
- No breaking API changes, but static readonly options fields are now properties
This commit is contained in:
Loretta 2026-01-25 16:40:40 +01:00
parent 145cc0a493
commit 1a77ee4bf9
43 changed files with 816 additions and 396 deletions

View File

@ -177,7 +177,7 @@ namespace AyCode.Benchmark
// Options // Options
var withRefOptions = new AcBinarySerializerOptions(); var withRefOptions = new AcBinarySerializerOptions();
var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
var msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); var msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
// Warm up // Warm up
@ -364,7 +364,7 @@ namespace AyCode.Benchmark
Console.WriteLine($"Created order with {order.Items.Count} items"); Console.WriteLine($"Created order with {order.Items.Count} items");
Console.WriteLine("\nTesting JSON serialization..."); Console.WriteLine("\nTesting JSON serialization...");
var jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling(); var jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling;
var json = AcJsonSerializer.Serialize(order, jsonOptions); var json = AcJsonSerializer.Serialize(order, jsonOptions);
// Log a quick summary to Out folder for convenience // Log a quick summary to Out folder for convenience

View File

@ -14,6 +14,7 @@ using MongoDB.Bson.Serialization;
using System.IO; using System.IO;
using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Serializers.Jsons; using AyCode.Core.Serializers.Jsons;
using AyCode.Core.Serializers;
namespace AyCode.Core.Benchmarks; namespace AyCode.Core.Benchmarks;
@ -59,23 +60,23 @@ public class SimpleBinaryBenchmark
public void Setup() public void Setup()
{ {
_testData = TestDataFactory.CreatePrimitiveTestData(); _testData = TestDataFactory.CreatePrimitiveTestData();
_binaryData = AcBinarySerializer.Serialize(_testData, AcBinarySerializerOptions.WithoutReferenceHandling()); _binaryData = AcBinarySerializer.Serialize(_testData, AcBinarySerializerOptions.WithoutReferenceHandling);
_jsonData = AcJsonSerializer.Serialize(_testData, AcJsonSerializerOptions.WithoutReferenceHandling()); _jsonData = AcJsonSerializer.Serialize(_testData, AcJsonSerializerOptions.WithoutReferenceHandling);
Console.WriteLine($"Binary: {_binaryData.Length} bytes, JSON: {_jsonData.Length} chars"); Console.WriteLine($"Binary: {_binaryData.Length} bytes, JSON: {_jsonData.Length} chars");
} }
[Benchmark(Description = "Binary Serialize")] [Benchmark(Description = "Binary Serialize")]
public byte[] SerializeBinary() => AcBinarySerializer.Serialize(_testData, AcBinarySerializerOptions.WithoutReferenceHandling()); public byte[] SerializeBinary() => AcBinarySerializer.Serialize(_testData, AcBinarySerializerOptions.WithoutReferenceHandling);
[Benchmark(Description = "JSON Serialize", Baseline = true)] [Benchmark(Description = "JSON Serialize", Baseline = true)]
public string SerializeJson() => AcJsonSerializer.Serialize(_testData, AcJsonSerializerOptions.WithoutReferenceHandling()); public string SerializeJson() => AcJsonSerializer.Serialize(_testData, AcJsonSerializerOptions.WithoutReferenceHandling);
[Benchmark(Description = "Binary Deserialize")] [Benchmark(Description = "Binary Deserialize")]
public PrimitiveTestClass? DeserializeBinary() => AcBinaryDeserializer.Deserialize<PrimitiveTestClass>(_binaryData); public PrimitiveTestClass? DeserializeBinary() => AcBinaryDeserializer.Deserialize<PrimitiveTestClass>(_binaryData);
[Benchmark(Description = "JSON Deserialize")] [Benchmark(Description = "JSON Deserialize")]
public PrimitiveTestClass? DeserializeJson() => AcJsonDeserializer.Deserialize<PrimitiveTestClass>(_jsonData, AcJsonSerializerOptions.WithoutReferenceHandling()); public PrimitiveTestClass? DeserializeJson() => AcJsonDeserializer.Deserialize<PrimitiveTestClass>(_jsonData, AcJsonSerializerOptions.WithoutReferenceHandling);
} }
/// <summary> /// <summary>
@ -105,8 +106,8 @@ public class ComplexBinaryBenchmark
pointsPerMeasurement: 3); pointsPerMeasurement: 3);
Console.WriteLine($"Created order with {_testOrder.Items.Count} items"); Console.WriteLine($"Created order with {_testOrder.Items.Count} items");
_binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); _binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
_jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling(); _jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling;
Console.WriteLine("Serializing AcBinary..."); Console.WriteLine("Serializing AcBinary...");
_acBinaryData = AcBinarySerializer.Serialize(_testOrder, _binaryOptions); _acBinaryData = AcBinarySerializer.Serialize(_testOrder, _binaryOptions);
@ -163,9 +164,9 @@ public class MessagePackComparisonBenchmark
measurementsPerPallet: 2, measurementsPerPallet: 2,
pointsPerMeasurement: 3); pointsPerMeasurement: 3);
_binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); _binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
_msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); _msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
_jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling(); _jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling;
_acBinaryData = AcBinarySerializer.Serialize(_testOrder, _binaryOptions); _acBinaryData = AcBinarySerializer.Serialize(_testOrder, _binaryOptions);
_jsonData = AcJsonSerializer.Serialize(_testOrder, _jsonOptions); _jsonData = AcJsonSerializer.Serialize(_testOrder, _jsonOptions);
@ -276,7 +277,7 @@ public class AcBinaryVsMessagePackFullBenchmark
// Setup options - WithRef uses Default (which has reference handling), NoRef explicitly disables it // Setup options - WithRef uses Default (which has reference handling), NoRef explicitly disables it
_withRefOptions = AcBinarySerializerOptions.Default; _withRefOptions = AcBinarySerializerOptions.Default;
_noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); _noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
_msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); _msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
// Serialize with different options // Serialize with different options
@ -423,8 +424,8 @@ public class SizeComparisonBenchmark
public void Setup() public void Setup()
{ {
_msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); _msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
_withRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); _withRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
_noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); _noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
// Small order // Small order
TestDataFactory.ResetIdCounter(); TestDataFactory.ResetIdCounter();
@ -530,7 +531,7 @@ public abstract class AcBinaryOptionsBenchmarkBase
private static AcBinarySerializerOptions CreateBinaryOptions(BinaryBenchmarkMode mode) => mode switch private static AcBinarySerializerOptions CreateBinaryOptions(BinaryBenchmarkMode mode) => mode switch
{ {
BinaryBenchmarkMode.Default => new AcBinarySerializerOptions(), BinaryBenchmarkMode.Default => new AcBinarySerializerOptions(),
BinaryBenchmarkMode.NoReferenceHandling => AcBinarySerializerOptions.WithoutReferenceHandling(), BinaryBenchmarkMode.NoReferenceHandling => AcBinarySerializerOptions.WithoutReferenceHandling,
BinaryBenchmarkMode.FastMode => new AcBinarySerializerOptions BinaryBenchmarkMode.FastMode => new AcBinarySerializerOptions
{ {
UseMetadata = false, UseMetadata = false,
@ -608,7 +609,7 @@ public class LargeScaleBinaryBenchmark
_testOrder = TestDataFactory.CreateLargeScaleBenchmarkOrder(rootItems, pallets, measurements, points); _testOrder = TestDataFactory.CreateLargeScaleBenchmarkOrder(rootItems, pallets, measurements, points);
Console.WriteLine($"Created order with {_testOrder.Items.Count} root items"); Console.WriteLine($"Created order with {_testOrder.Items.Count} root items");
_binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); _binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
_msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); _msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
Console.WriteLine("Serializing AcBinary..."); Console.WriteLine("Serializing AcBinary...");
@ -677,7 +678,7 @@ public class AcJsonVsSystemTextJsonBenchmark
_testData = TestDataFactory.CreatePrimitiveTestData(); _testData = TestDataFactory.CreatePrimitiveTestData();
// Setup options // Setup options
_acJsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling(); _acJsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling;
_stjOptions = new JsonSerializerOptions _stjOptions = new JsonSerializerOptions
{ {
WriteIndented = false, WriteIndented = false,

View File

@ -83,7 +83,7 @@ public class PureContractlessBenchmark
Status = "Available" Status = "Available"
}; };
_binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); _binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
_msgPackContractlessOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); _msgPackContractlessOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
_acBinaryData = AcBinarySerializer.Serialize(_testData, _binaryOptions); _acBinaryData = AcBinarySerializer.Serialize(_testData, _binaryOptions);
@ -148,7 +148,7 @@ public class SourceGeneratorVsRuntimeBenchmark
Status = "Available" Status = "Available"
}; };
_binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); _binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
// MessagePack with Source Generator (uses [MessagePackObject] + [Key] attributes) // MessagePack with Source Generator (uses [MessagePackObject] + [Key] attributes)
_msgPackOptions = MessagePackSerializerOptions.Standard; _msgPackOptions = MessagePackSerializerOptions.Standard;
@ -254,7 +254,7 @@ public class RepeatedStringBenchmark
Type = i % 4 == 0 ? "TypeA" : i % 4 == 1 ? "TypeB" : i % 4 == 2 ? "TypeC" : "TypeD" Type = i % 4 == 0 ? "TypeA" : i % 4 == 1 ? "TypeB" : i % 4 == 2 ? "TypeC" : "TypeD"
}).ToList(); }).ToList();
_binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); _binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
_msgPackOptions = MessagePackSerializerOptions.Standard; _msgPackOptions = MessagePackSerializerOptions.Standard;
_msgPackContractlessOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); _msgPackContractlessOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);

View File

@ -43,7 +43,7 @@ public static class Program
private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false); private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
private static int WarmupIterations = 5; private static int WarmupIterations = 2000;
private static int TestIterations = 1000; private static int TestIterations = 1000;
public static void Main(string[] args) public static void Main(string[] args)
@ -90,8 +90,6 @@ public static class Program
// Print grouped results // Print grouped results
PrintGroupedResults(allResults, testDataSets); PrintGroupedResults(allResults, testDataSets);
// Save results to file // Save results to file
SaveResults(allResults, testDataSets); SaveResults(allResults, testDataSets);
@ -122,7 +120,8 @@ public static class Program
sharedTag: sharedTag, sharedTag: sharedTag,
sharedUser: sharedUser); sharedUser: sharedUser);
var options = AcBinarySerializerOptions.WithoutReferenceHandling(); var options = AcBinarySerializerOptions.WithoutReferenceHandling;
options.UseStringInterning = false;
// Warmup (fills caches) // Warmup (fills caches)
System.Console.WriteLine("Warming up (10 iterations)..."); System.Console.WriteLine("Warming up (10 iterations)...");
@ -132,6 +131,16 @@ public static class Program
} }
System.Console.WriteLine("Warmup complete. Caches are now populated."); System.Console.WriteLine("Warmup complete. Caches are now populated.");
System.Console.WriteLine(); System.Console.WriteLine();
// HOT PATH - this is what the profiler should capture!
System.Console.WriteLine("Running hot path (1000 iterations for profiling)...");
for (var i = 0; i < 1000; i++)
{
_ = AcBinarySerializer.Serialize(order, options);
}
System.Console.WriteLine("Hot path complete.");
System.Console.WriteLine();
System.Console.WriteLine(">>> ATTACH MEMORY PROFILER NOW <<<"); System.Console.WriteLine(">>> ATTACH MEMORY PROFILER NOW <<<");
System.Console.WriteLine("Press any key to exit..."); System.Console.WriteLine("Press any key to exit...");
System.Console.ReadKey(intercept: true); System.Console.ReadKey(intercept: true);
@ -369,6 +378,9 @@ public static class Program
serializer.Warmup(WarmupIterations); serializer.Warmup(WarmupIterations);
} }
// Wait for tiered JIT background compilation to complete
Thread.Sleep(2000);
// Run benchmarks // Run benchmarks
System.Console.WriteLine($"Running benchmarks ({TestIterations} iterations)...\n"); System.Console.WriteLine($"Running benchmarks ({TestIterations} iterations)...\n");
@ -402,9 +414,10 @@ public static class Program
{ {
return new List<ISerializerBenchmark> return new List<ISerializerBenchmark>
{ {
// AcBinary variants // AcBinary variants
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault), new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault),
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling(), SerializerAcBinaryNoRef), new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling, SerializerAcBinaryNoRef),
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode), new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
new AcBinaryBenchmark(testData.Order, new AcBinarySerializerOptions { UseStringInterning = false }, SerializerAcBinaryNoIntern), new AcBinaryBenchmark(testData.Order, new AcBinarySerializerOptions { UseStringInterning = false }, SerializerAcBinaryNoIntern),

View File

@ -1,6 +1,6 @@
using AyCode.Core.Extensions; using AyCode.Core.Extensions;
using AyCode.Core.Serializers;
using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Serializers.Jsons;
using static AyCode.Core.Tests.TestModels.AcSerializerModels; using static AyCode.Core.Tests.TestModels.AcSerializerModels;
namespace AyCode.Core.Tests.Serialization; namespace AyCode.Core.Tests.Serialization;

View File

@ -2,8 +2,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using AyCode.Core.Extensions; using AyCode.Core.Extensions;
using AyCode.Core.Serializers;
using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Serializers.Jsons;
using AyCode.Core.Tests.TestModels; using AyCode.Core.Tests.TestModels;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;

View File

@ -109,7 +109,7 @@ public class GeneratedSerializerIntegrationTests
}; };
// Serialize and deserialize using the regular path // Serialize and deserialize using the regular path
var bytes = AcBinarySerializer.Serialize(original, AcBinarySerializerOptions.WithoutReferenceHandling()); var bytes = AcBinarySerializer.Serialize(original, AcBinarySerializerOptions.WithoutReferenceHandling);
var deserialized = AcBinaryDeserializer.Deserialize<GeneratedSerializerTestModel>(bytes); var deserialized = AcBinaryDeserializer.Deserialize<GeneratedSerializerTestModel>(bytes);
// Assert // Assert

View File

@ -1,5 +1,6 @@
using System.Diagnostics; using System.Diagnostics;
using AyCode.Core.Extensions; using AyCode.Core.Extensions;
using AyCode.Core.Serializers;
using AyCode.Core.Serializers.Attributes; using AyCode.Core.Serializers.Attributes;
using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Serializers.Jsons; using AyCode.Core.Serializers.Jsons;
@ -12,7 +13,7 @@ namespace AyCode.Core.Tests.Serialization;
[TestClass] [TestClass]
public class QuickBenchmark public class QuickBenchmark
{ {
private static readonly MessagePackSerializerOptions MsgPackOptions = private static readonly MessagePackSerializerOptions MsgPackOptions =
ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
private const int DefaultIterations = 1000; private const int DefaultIterations = 1000;
@ -129,7 +130,7 @@ public class QuickBenchmark
var deserializeMs = sw.Elapsed.TotalMilliseconds; var deserializeMs = sw.Elapsed.TotalMilliseconds;
// JSON comparison // JSON comparison
var jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling(); var jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling;
sw.Restart(); sw.Restart();
string json = null!; string json = null!;
for (int i = 0; i < iterations; i++) for (int i = 0; i < iterations; i++)
@ -184,7 +185,7 @@ public class QuickBenchmark
} }
const int iterations = DefaultIterations; const int iterations = DefaultIterations;
// With interning (default) // With interning (default)
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
byte[] withInterning = null!; byte[] withInterning = null!;
@ -290,7 +291,7 @@ public class QuickBenchmark
Console.WriteLine($"{"Deserialize (ms)",-25} {acBinaryDeserMs,12:F2} {msgPackDeserMs,12:F2} {acBinaryDeserMs / msgPackDeserMs,9:F2}x"); Console.WriteLine($"{"Deserialize (ms)",-25} {acBinaryDeserMs,12:F2} {msgPackDeserMs,12:F2} {acBinaryDeserMs / msgPackDeserMs,9:F2}x");
Console.WriteLine($"{"Round-trip (ms)",-25} {acBinarySerMs + acBinaryDeserMs,12:F2} {msgPackSerMs + msgPackDeserMs,12:F2} {(acBinarySerMs + acBinaryDeserMs) / (msgPackSerMs + msgPackDeserMs),9:F2}x"); Console.WriteLine($"{"Round-trip (ms)",-25} {acBinarySerMs + acBinaryDeserMs,12:F2} {msgPackSerMs + msgPackDeserMs,12:F2} {(acBinarySerMs + acBinaryDeserMs) / (msgPackSerMs + msgPackDeserMs),9:F2}x");
Console.WriteLine(); Console.WriteLine();
var sizeDiff = msgPackData.Length - acBinaryData.Length; var sizeDiff = msgPackData.Length - acBinaryData.Length;
if (sizeDiff > 0) if (sizeDiff > 0)
Console.WriteLine($"[OK] AcBinary {sizeDiff:N0} bytes smaller ({100.0 * sizeDiff / msgPackData.Length:F1}% savings)"); Console.WriteLine($"[OK] AcBinary {sizeDiff:N0} bytes smaller ({100.0 * sizeDiff / msgPackData.Length:F1}% savings)");
@ -376,6 +377,114 @@ public class QuickBenchmark
#region Full Comparison (WithRef, NoRef, Populate, Merge) #region Full Comparison (WithRef, NoRef, Populate, Merge)
#if DEBUG
[TestMethod]
public void GetAnalyzeStringInternCandidatesLog()
{
TestDataFactory.ResetIdCounter();
var sharedTag = TestDataFactory.CreateTag("SharedTag");
var sharedUser = TestDataFactory.CreateUser("shareduser");
var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true);
var testOrder = TestDataFactory.CreateOrder(
itemCount: 3,
palletsPerItem: 3,
measurementsPerPallet: 3,
pointsPerMeasurement: 4,
sharedTag: sharedTag,
sharedUser: sharedUser,
sharedMetadata: sharedMeta);
var options = AcBinarySerializerOptions.Default;
options.ReferenceHandling = ReferenceHandlingMode.OnlyId;
var analysisLog = AcBinarySerializer.GetAnalyzeStringInternCandidatesLog(testOrder,options);
Assert.IsNotNull(analysisLog);
Assert.IsGreaterThan(0, analysisLog.Length);
// Print results sorted by occurrence count
Console.WriteLine(analysisLog.ToString());
Console.WriteLine();
}
#endif
[TestMethod]
public void RunFullBenchmarkComparison2()
{
// Create test data with shared references
TestDataFactory.ResetIdCounter();
var sharedTag = TestDataFactory.CreateTag("SharedTag");
var sharedUser = TestDataFactory.CreateUser("shareduser");
var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true);
var testOrder = TestDataFactory.CreateOrder(
itemCount: 3,
palletsPerItem: 3,
measurementsPerPallet: 3,
pointsPerMeasurement: 4,
sharedTag: sharedTag,
sharedUser: sharedUser,
sharedMetadata: sharedMeta);
var singleOptions = AcBinarySerializerOptions.FastMode;
singleOptions.UseStringInterning = false;
Console.WriteLine("=== MINIMAL WARMUP TEST ===");
Console.WriteLine();
// Wait for tiered JIT
//Console.WriteLine("Waiting 3s for tiered JIT...");
// MINIMAL WARMUP: Just 1 call to populate caches
Console.WriteLine("Single warmup call (cache only)...");
for (int i = 0; i < DefaultIterations; i++)
{
_ = AcBinarySerializer.Serialize(testOrder, singleOptions);
_ = MessagePackSerializer.Serialize(testOrder, MsgPackOptions);
}
Thread.Sleep(2000);
Console.WriteLine();
Console.WriteLine("--- MEASURED TESTS (5x) ---");
var sw = Stopwatch.StartNew();
var results = new double[5];
for (int test = 0; test < 5; test++)
{
sw.Restart();
for (int i = 0; i < DefaultIterations; i++)
_ = AcBinarySerializer.Serialize(testOrder, singleOptions);
results[test] = sw.Elapsed.TotalMilliseconds;
Console.WriteLine($"AcBinary Test{test + 1}: {results[test]:F2}ms");
}
Console.WriteLine();
var msgResults = new double[5];
for (int test = 0; test < 5; test++)
{
sw.Restart();
for (int i = 0; i < DefaultIterations; i++)
_ = MessagePackSerializer.Serialize(testOrder, MsgPackOptions);
msgResults[test] = sw.Elapsed.TotalMilliseconds;
Console.WriteLine($"MsgPack Test{test + 1}: {msgResults[test]:F2}ms");
}
Console.WriteLine();
Console.WriteLine("--- ANALYSIS ---");
var acBinaryAvg = results.Average();
var acBinaryVariance = results.Max() - results.Min();
var msgPackAvg = msgResults.Average();
var msgPackVariance = msgResults.Max() - msgResults.Min();
Console.WriteLine($"AcBinary: avg={acBinaryAvg:F2}ms, variance={acBinaryVariance:F2}ms ({100 * acBinaryVariance / acBinaryAvg:F1}%)");
Console.WriteLine($"MsgPack: avg={msgPackAvg:F2}ms, variance={msgPackVariance:F2}ms ({100 * msgPackVariance / msgPackAvg:F1}%)");
var isVarianceOk = acBinaryVariance <= acBinaryAvg * 0.2;
Console.WriteLine(isVarianceOk
? "[OK] Minimal warmup (1 call + 3s delay) is SUFFICIENT!"
: "[PROBLEM] Minimal warmup is NOT enough - need more iterations");
}
[TestMethod] [TestMethod]
public void RunFullBenchmarkComparison() public void RunFullBenchmarkComparison()
{ {
@ -397,23 +506,32 @@ public class QuickBenchmark
sharedMetadata: sharedMeta); sharedMetadata: sharedMeta);
// Options // Options
var withRefOptions = new AcBinarySerializerOptions(); var withRefOptions = AcBinarySerializerOptions.Default;
var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); //withRefOptions.UseStringInterning = false;
var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
// Warmup noRefOptions.UseStringInterning = false;
Console.WriteLine("\nWarming up...");
for (int i = 0; i < 10; i++)
{
_ = AcBinarySerializer.Serialize(testOrder, withRefOptions);
_ = AcBinarySerializer.Serialize(testOrder, noRefOptions);
_ = MessagePackSerializer.Serialize(testOrder, MsgPackOptions);
}
// Pre-serialize // Pre-serialize
var acBinaryWithRef = AcBinarySerializer.Serialize(testOrder, withRefOptions); var acBinaryWithRef = AcBinarySerializer.Serialize(testOrder, withRefOptions);
var acBinaryNoRef = AcBinarySerializer.Serialize(testOrder, noRefOptions); var acBinaryNoRef = AcBinarySerializer.Serialize(testOrder, noRefOptions);
var msgPackData = MessagePackSerializer.Serialize(testOrder, MsgPackOptions); var msgPackData = MessagePackSerializer.Serialize(testOrder, MsgPackOptions);
// Warmup - MUST be 1000+ iterations for tiered JIT to complete
Console.WriteLine($"\nSerialize warming up ({DefaultIterations} iterations each)...");
for (int i = 0; i < 500; i++)
{
_ = AcBinarySerializer.Serialize(testOrder, withRefOptions);
_ = AcBinarySerializer.Serialize(testOrder, noRefOptions);
_ = MessagePackSerializer.Serialize(testOrder, MsgPackOptions);
_ = AcBinaryDeserializer.Deserialize<TestOrder>(acBinaryWithRef);
_ = AcBinaryDeserializer.Deserialize<TestOrder>(acBinaryNoRef);
_ = MessagePackSerializer.Deserialize<TestOrder>(msgPackData, MsgPackOptions);
}
// Wait for tiered JIT background compilation to complete
Thread.Sleep(2000);
Console.WriteLine($"Iterations: {DefaultIterations:N0}"); Console.WriteLine($"Iterations: {DefaultIterations:N0}");
// Size comparison // Size comparison
@ -519,7 +637,7 @@ public class QuickBenchmark
sharedMetadata: sharedMeta); sharedMetadata: sharedMeta);
var withRefOptions = new AcBinarySerializerOptions(); var withRefOptions = new AcBinarySerializerOptions();
var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
// Warmup // Warmup
Console.WriteLine("Warming up..."); Console.WriteLine("Warming up...");
@ -602,7 +720,7 @@ public class QuickBenchmark
measurementsPerPallet: 3, measurementsPerPallet: 3,
pointsPerMeasurement: 4); pointsPerMeasurement: 4);
var options = AcBinarySerializerOptions.WithoutReferenceHandling(); var options = AcBinarySerializerOptions.WithoutReferenceHandling;
var binaryData = AcBinarySerializer.Serialize(testOrder, options); var binaryData = AcBinarySerializer.Serialize(testOrder, options);
Console.WriteLine("Warming up..."); Console.WriteLine("Warming up...");

View File

@ -6,6 +6,7 @@ using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using AyCode.Core.Interfaces; using AyCode.Core.Interfaces;
using AyCode.Core.Serializers;
using AyCode.Core.Serializers.Jsons; using AyCode.Core.Serializers.Jsons;
using Newtonsoft.Json; using Newtonsoft.Json;

View File

@ -4,7 +4,6 @@ using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using AyCode.Core.Serializers.Expressions; using AyCode.Core.Serializers.Expressions;
using AyCode.Core.Serializers.Jsons;
using LExpression = System.Linq.Expressions.Expression; using LExpression = System.Linq.Expressions.Expression;
using LExpressionType = System.Linq.Expressions.ExpressionType; using LExpressionType = System.Linq.Expressions.ExpressionType;
@ -538,8 +537,13 @@ public static class AcSerializerCommon
var objParam = LExpression.Parameter(typeof(object), "obj"); var objParam = LExpression.Parameter(typeof(object), "obj");
var castExpr = LExpression.Convert(objParam, declaringType); var castExpr = LExpression.Convert(objParam, declaringType);
var propAccess = LExpression.Property(castExpr, prop); var propAccess = LExpression.Property(castExpr, prop);
var convertExpr = LExpression.Convert(propAccess, typeof(TProperty));
return LExpression.Lambda<Func<object, TProperty>>(convertExpr, objParam).Compile(); // Only convert if property type differs from TProperty (avoids unnecessary boxing)
Expression resultExpr = prop.PropertyType == typeof(TProperty)
? propAccess
: LExpression.Convert(propAccess, typeof(TProperty));
return LExpression.Lambda<Func<object, TProperty>>(resultExpr, objParam).Compile();
} }
/// <summary> /// <summary>

View File

@ -1,4 +1,3 @@
using AyCode.Core.Serializers.Jsons;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;

View File

@ -0,0 +1,88 @@
using System.Reflection;
namespace AyCode.Core.Serializers;
public abstract class AcSerializerOptions
{
public abstract AcSerializerType SerializerType { get; init; }
/// <summary>
/// Reference handling mode for circular/shared references.
/// Default: OnlyId (JSON serializer requires All mode, OnlyId not yet implemented)
/// Note: Binary serializer supports OnlyId mode for IId-only tracking.
/// </summary>
public ReferenceHandlingMode ReferenceHandling { get; set; } = ReferenceHandlingMode.OnlyId;
/// <summary>
/// Maximum depth for serialization/deserialization.
/// 0 = root level only (primitives of root object)
/// 1 = root + first level of nested objects/collections
/// byte.MaxValue (255) = effectively unlimited
/// Default: byte.MaxValue
/// </summary>
public byte MaxDepth { get; init; } = byte.MaxValue;
/// <summary>
/// Throw exception on circular reference detection for non-IId types.
/// When true: Tracks all objects and throws InvalidOperationException on circular references.
/// When false: No tracking for non-IId types (faster, but circular refs may cause MaxDepth truncation).
/// Default: true (production safety)
/// Note: IId types are always tracked when ReferenceHandling != None.
/// </summary>
public bool ThrowOnCircularReference { get; init; } = true;
/// <summary>
/// Optional callback for custom property mapping during cross-type operations.
/// Used when deserializing/populating with Deserialize&lt;TSource, TDest&gt; or Populate&lt;TSource, TDest&gt;.
///
/// Use cases:
/// - Mapping between external DTOs and internal models (different class hierarchies)
/// - Handling property renames across versions
/// - Custom property pairing logic
///
/// If null (default), properties are matched by name.
/// Callback is invoked once during mapping build phase and result is cached.
///
/// Performance: ZERO overhead on same-type operations (Deserialize&lt;T&gt;).
/// </summary>
public PropertyMapperDelegate? PropertyMapper { get; init; }
}
public enum AcSerializerType : byte
{
Json = 0,
Binary = 1,
Toon = 2,
}
/// <summary>
/// Reference handling mode for serialization.
/// </summary>
public enum ReferenceHandlingMode : byte
{
/// <summary>
/// No reference handling - all objects serialized inline.
/// </summary>
None = 0,
/// <summary>
/// Reference handling only for IId objects - uses semantic Id for deduplication.
/// NOTE: Not fully implemented for JSON serializer - use All instead.
/// Binary serializer supports this mode.
/// </summary>
OnlyId = 1,
/// <summary>
/// Full reference handling for all objects.
/// </summary>
All = 2
}
/// <summary>
/// Delegate for custom property mapping during cross-type deserialization/population.
/// Enables mapping between different class hierarchies or renamed properties.
/// </summary>
/// <param name="sourceProperty">Property from the source type being deserialized</param>
/// <param name="destinationType">Target type being populated</param>
/// <returns>Mapped destination property, or null to skip this property</returns>
public delegate PropertyInfo? PropertyMapperDelegate(PropertyInfo sourceProperty, Type destinationType);

View File

@ -1,5 +1,4 @@
using AyCode.Core.Serializers.Jsons; using System;
using System;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;

View File

@ -1,6 +1,5 @@
using System.Collections; using System.Collections;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using AyCode.Core.Serializers.Jsons;
using static AyCode.Core.Serializers.AcSerializerCommon; using static AyCode.Core.Serializers.AcSerializerCommon;
namespace AyCode.Core.Serializers.Binaries; namespace AyCode.Core.Serializers.Binaries;

View File

@ -1,4 +1,3 @@
using AyCode.Core.Serializers.Jsons;
using System; using System;
using System.Buffers; using System.Buffers;
using System.Collections.Concurrent; using System.Collections.Concurrent;
@ -77,6 +76,21 @@ public static partial class AcBinarySerializer
private int[]? _propertyIndexBuffer; private int[]? _propertyIndexBuffer;
private byte[]? _propertyStateBuffer; private byte[]? _propertyStateBuffer;
#if DEBUG
/// <summary>
/// DEBUG ONLY: Current property path being serialized (e.g., "Order.Status").
/// Used for string interning analysis.
/// </summary>
internal string? CurrentPropertyPath;
/// <summary>
/// DEBUG ONLY: Callback invoked when a string is registered for interning.
/// Parameters: (propertyPath, stringValue)
/// Use this to analyze which properties have repeated string values.
/// </summary>
internal Action<string?, string>? OnStringInterned;
#endif
// These properties delegate to Options for convenience // These properties delegate to Options for convenience
public bool UseStringInterning => Options.UseStringInterning; public bool UseStringInterning => Options.UseStringInterning;
public bool UseMetadata => Options.UseMetadata; public bool UseMetadata => Options.UseMetadata;
@ -1161,6 +1175,48 @@ public static partial class AcBinarySerializer
_position += length; _position += length;
} }
/// <summary>
/// Optimized FixStr write: tries SIMD ASCII conversion, falls back to UTF8.
/// Single-pass: uses Ascii.FromUtf16 which does validation + copy.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteFixStrDirect(string value)
{
var length = value.Length;
EnsureCapacity(1 + length);
// Ascii.FromUtf16: SIMD-optimized ASCII conversion
// Returns actual bytes written - if less than input length, there was a non-ASCII char
var destSpan = _buffer.AsSpan(_position + 1, length);
var status = Ascii.FromUtf16(value.AsSpan(), destSpan, out var bytesWritten);
if (status == System.Buffers.OperationStatus.Done && bytesWritten == length)
{
// Success - write FixStr header
_buffer[_position] = BinaryTypeCode.EncodeFixStr(length);
_position += 1 + length;
}
else
{
// Non-ASCII or partial - use standard string encoding
_buffer[_position++] = BinaryTypeCode.String;
WriteStringUtf8Internal(value);
}
}
/// <summary>
/// Internal string write (after String type code already written).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void WriteStringUtf8Internal(string value)
{
var byteCount = Utf8NoBom.GetByteCount(value);
WriteVarUInt((uint)byteCount);
EnsureCapacity(byteCount);
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
_position += byteCount;
}
/// <summary> /// <summary>
/// Write short UTF8 bytes using FixStr encoding. /// Write short UTF8 bytes using FixStr encoding.
/// Only call when byteLength <= 31. /// Only call when byteLength <= 31.

View File

@ -1,4 +1,4 @@
using AyCode.Core.Helpers; using AyCode.Core.Helpers;
using AyCode.Core.Serializers.Expressions; using AyCode.Core.Serializers.Expressions;
using System.Buffers; using System.Buffers;
using System.Collections; using System.Collections;
@ -44,6 +44,177 @@ public static partial class AcBinarySerializer
#region Public API #region Public API
#if DEBUG
/// <summary>
/// DEBUG ONLY: Analyzes which string properties have repeated values that would benefit from interning.
/// Returns a dictionary where key is "TypeName.PropertyName" and value is the occurrence count.
/// Only properties with count > 1 are good candidates for [StringIntern] attribute.
/// </summary>
/// <param name="value">The object graph to analyze.</param>
/// <param name="options">Serializer options (UseStringInterning should be enabled).</param>
/// <returns>Dictionary of property paths to their string occurrence counts.</returns>
public static Dictionary<string, Dictionary<string, int>> AnalyzeStringInternCandidates<T>(T value, AcBinarySerializerOptions? options = null)
{
ArgumentNullException.ThrowIfNull(value);
options ??= AcBinarySerializerOptions.Default;
// For analysis, use the provided reference handling mode
var analysisOptions = new AcBinarySerializerOptions
{
UseStringInterning = true,
MinStringInternLength = options.MinStringInternLength,
MaxStringInternLength = options.MaxStringInternLength,
ReferenceHandling = options.ReferenceHandling
};
var result = new Dictionary<string, Dictionary<string, int>>();
var runtimeType = value.GetType();
// Create context without pooling (we need to set up callback)
using var context = new BinarySerializationContext(analysisOptions);
// Set up tracking callbacks
context.OnStringInterned = (propertyPath, stringValue) =>
{
propertyPath ??= "(unknown)";
if (!result.TryGetValue(stringValue, out var properties))
{
properties = new Dictionary<string, int>();
result[stringValue] = properties;
}
properties[propertyPath] = properties.GetValueOrDefault(propertyPath) + 1;
};
// Run serialization to trigger callbacks
context.WriteHeaderPlaceholder();
var estimatedHeaderSize = context.EstimateHeaderPayloadSize();
context.ReserveHeaderSpace(estimatedHeaderSize);
WriteValue(value, runtimeType, context, 0);
context.FinalizeHeaderSections();
return result;
}
public static StringBuilder GetAnalyzeStringInternCandidatesLog<T>(T value, AcBinarySerializerOptions? options = null)
{
var sb = new StringBuilder();
options ??= AcBinarySerializerOptions.Default;
var analysis = AnalyzeStringInternCandidates(value, options);
// Transform: stringValue → properties TO propertyPath → (stringValue, count)
var propertyStats = new Dictionary<string, List<(string StringValue, int Count, int ByteLength)>>();
foreach (var (stringValue, properties) in analysis)
{
var byteLength = Encoding.UTF8.GetByteCount(stringValue);
foreach (var (propPath, count) in properties)
{
if (!propertyStats.TryGetValue(propPath, out var list))
{
list = [];
propertyStats[propPath] = list;
}
list.Add((stringValue, count, byteLength));
}
}
var refMode = options.ReferenceHandling;
// Header
sb.AppendLine("+==============================================================================+");
sb.AppendLine($"| STRING INTERN ANALYSIS REPORT (Mode: {refMode,-12}) |");
sb.AppendLine("+==============================================================================+");
sb.AppendLine();
// Global summary
var totalStrings = analysis.Values.Sum(p => p.Values.Sum());
var uniqueStrings = analysis.Count;
var repeatedStrings = analysis.Count(kv => kv.Value.Values.Sum() > 1);
sb.AppendLine("+-----------------------------------------------------------------------------+");
sb.AppendLine("| STRING SUMMARY |");
sb.AppendLine("+-----------------------------------------------------------------------------+");
sb.AppendLine($"| Total string occurrences: {totalStrings,-10} Unique strings: {uniqueStrings,-10} Repeated: {repeatedStrings,-8} |");
sb.AppendLine("+-----------------------------------------------------------------------------+");
sb.AppendLine();
// Property-focused table
// Calculate stats for each property first (for sorting by RepeatSum%)
var propertyStatsCalculated = propertyStats.Select(kv =>
{
var propPath = kv.Key;
var strings = kv.Value;
var total = strings.Sum(s => s.Count);
var unique = strings.Count;
var repeated = strings.Count(s => s.Count > 1);
var repeatSum = strings.Where(s => s.Count > 1).Sum(s => s.Count); // Sum of occurrences of repeated strings
var repeatSumPct = total > 0 ? repeatSum * 100.0 / total : 0;
// Calculate savings
var totalBytes = strings.Sum(s => s.Count * s.ByteLength);
var uniqueBytes = strings.Sum(s => s.ByteLength);
var indexBytes = total * 2;
var savings = totalBytes - (uniqueBytes + indexBytes);
return (propPath, strings, total, unique, repeated, repeatSum, repeatSumPct, savings);
}).ToList();
sb.AppendLine("+--------------------------------+-------+---------+--------+---------+-----------+----------+-------------+");
sb.AppendLine("| Property | Total | RepSum | Unique | Repeated| RepSum % | Savings | Recommend |");
sb.AppendLine("+--------------------------------+-------+---------+--------+---------+-----------+----------+-------------+");
foreach (var stat in propertyStatsCalculated.OrderByDescending(x => x.repeatSumPct))
{
var savingsStr = stat.savings > 0 ? $"+{stat.savings:N0}B" : $"{stat.savings:N0}B";
var recommend = stat.savings > 100 ? "[INTERN]" : stat.savings > 0 ? " maybe " : " skip ";
var propDisplay = stat.propPath.Length > 30 ? stat.propPath[..27] + "..." : stat.propPath;
sb.AppendLine($"| {propDisplay,-30} | {stat.total,5} | {stat.repeatSum,7} | {stat.unique,6} | {stat.repeated,7} | {stat.repeatSumPct,8:F1}% | {savingsStr,8} | {recommend,-11} |");
}
sb.AppendLine("+--------------------------------+-------+---------+--------+---------+-----------+----------+-------------+");
sb.AppendLine();
// Detailed property breakdown (only for properties with significant savings)
sb.AppendLine("+-----------------------------------------------------------------------------+");
sb.AppendLine("| DETAILED BREAKDOWN (properties with savings > 100 bytes) |");
sb.AppendLine("+-----------------------------------------------------------------------------+");
foreach (var stat in propertyStatsCalculated
.Where(x => x.savings > 100)
.OrderByDescending(x => x.repeatSumPct))
{
sb.AppendLine();
sb.AppendLine($" {stat.propPath} (RepSum: {stat.repeatSum}/{stat.total} = {stat.repeatSumPct:F1}%):");
foreach (var (strVal, count, _) in stat.strings.OrderByDescending(s => s.Count).Take(10))
{
var preview = strVal.Length > 40 ? strVal[..37] + "..." : strVal;
var marker = count > 1 ? ">" : " ";
sb.AppendLine($" {marker} [{count,4}x] \"{preview}\"");
}
if (stat.strings.Count > 10)
{
sb.AppendLine($" ... and {stat.strings.Count - 10} more unique values");
}
}
sb.AppendLine();
sb.AppendLine("===============================================================================");
sb.AppendLine("Legend: Total=all occurrences, RepSum=sum of repeated string occurrences");
sb.AppendLine(" Unique=distinct values, Repeated=count of values appearing 2+ times");
sb.AppendLine(" RepSum%=percentage of occurrences that are repeated (higher=better for intern)");
sb.AppendLine(" Savings=estimated bytes saved with interning (positive=good)");
sb.AppendLine(" > = repeated string (benefits from interning)");
return sb;
}
#endif
/// <summary> /// <summary>
/// Serialize object to binary with default options. /// Serialize object to binary with default options.
/// </summary> /// </summary>
@ -607,19 +778,25 @@ public static partial class AcBinarySerializer
&& (context.MaxStringInternLength == 0 || value.Length <= context.MaxStringInternLength)) && (context.MaxStringInternLength == 0 || value.Length <= context.MaxStringInternLength))
{ {
var index = context.RegisterInternedString(value); var index = context.RegisterInternedString(value);
#if DEBUG
context.OnStringInterned?.Invoke(context.CurrentPropertyPath, value);
#endif
context.WriteByte(BinaryTypeCode.StringInterned); context.WriteByte(BinaryTypeCode.StringInterned);
context.WriteVarUInt((uint)index); context.WriteVarUInt((uint)index);
return; return;
} }
// Try FixStr for short ASCII strings (saves 1-2 bytes per string) // Fast path for short strings: check length first (cheap), then ASCII
if (System.Text.Ascii.IsValid(value) && BinaryTypeCode.CanEncodeAsFixStr(value.Length)) // FixStr encodes type+length in single byte for strings <= 31 chars
var length = value.Length;
if (length <= BinaryTypeCode.FixStrMaxLength)
{ {
context.WriteFixStr(value); // For short strings, use direct ASCII copy (avoids double validation)
context.WriteFixStrDirect(value);
return; return;
} }
// Standard string encoding // Long strings - standard encoding
context.WriteByte(BinaryTypeCode.String); context.WriteByte(BinaryTypeCode.String);
context.WriteStringUtf8(value); context.WriteStringUtf8(value);
} }
@ -642,29 +819,29 @@ public static partial class AcBinarySerializer
var metadata = wrapper.Metadata; var metadata = wrapper.Metadata;
// Wire format: // Wire format:
// - IId types: [Object][props 0-tól...] - Id a props-ban, nincs extra // - IId types: [Object][props 0-tól...] - Id a props-ban, nincs extra
// - Non-IId + All: [Object][hashcode][props 0-tól...] - hashcode elõre // - Non-IId + All: [Object][hashcode][props 0-tól...] - hashcode előre
// - Ref=Off: [Object][props 0-tól...] - semmi extra // - Ref=Off: [Object][props 0-tól...] - semmi extra
// ObjectRef format: // ObjectRef format:
// - IId: [ObjectRef][Id érték] // - IId: [ObjectRef][Id érték]
// - Non-IId: [ObjectRef][hashcode] // - Non-IId: [ObjectRef][hashcode]
if (context.UseTypeReferenceHandling(metadata)) if (context.UseTypeReferenceHandling(metadata))
{ {
if (metadata.IsIId) if (metadata.IsIId)
{ {
// IId típus: track by Id, ObjectRef writes Id // IId típus: track by Id, ObjectRef writes Id
switch (metadata.IdAccessorType) switch (metadata.IdAccessorType)
{ {
case AcSerializerCommon.IdAccessorType.Int32: case AcSerializerCommon.IdAccessorType.Int32:
if (!context.TryTrack(wrapper, value, out int intId)) if (!context.TryTrack(wrapper, value, out int intId))
{ {
// Already seen ? ObjectRef + Id // Already seen ObjectRef + Id
context.WriteByte(BinaryTypeCode.ObjectRef); context.WriteByte(BinaryTypeCode.ObjectRef);
context.WriteVarInt(intId); context.WriteVarInt(intId);
return; return;
} }
// First occurrence ? Object (no extra data, Id in props) // First occurrence Object (no extra data, Id in props)
context.WriteByte(BinaryTypeCode.Object); context.WriteByte(BinaryTypeCode.Object);
break; break;
@ -694,12 +871,12 @@ public static partial class AcBinarySerializer
// Non-IId + RefHandling=All: track by hashcode (IdAccessorType=Int32, RefIdGetter=GetHashCode) // Non-IId + RefHandling=All: track by hashcode (IdAccessorType=Int32, RefIdGetter=GetHashCode)
if (!context.TryTrack(wrapper, value, out int hashcode)) if (!context.TryTrack(wrapper, value, out int hashcode))
{ {
// Already seen ? ObjectRef + hashcode // Already seen ObjectRef + hashcode
context.WriteByte(BinaryTypeCode.ObjectRef); context.WriteByte(BinaryTypeCode.ObjectRef);
context.WriteVarInt(hashcode); context.WriteVarInt(hashcode);
return; return;
} }
// First occurrence ? Object + hashcode + props // First occurrence Object + hashcode + props
context.WriteByte(BinaryTypeCode.Object); context.WriteByte(BinaryTypeCode.Object);
context.WriteVarInt(hashcode); context.WriteVarInt(hashcode);
} }
@ -846,6 +1023,16 @@ public static partial class AcBinarySerializer
context.WriteVarInt(enumValue); context.WriteVarInt(enumValue);
} }
return; return;
case PropertyAccessorType.String:
{
// Fast path: typed getter, no boxing, no Type.GetTypeCode() call
var strValue = prop.GetString(obj);
if (strValue != null)
WriteString(strValue, context);
else
context.WriteByte(BinaryTypeCode.Null);
return;
}
default: default:
// Fallback to object getter for reference types // Fallback to object getter for reference types
var value = prop.GetValue(obj); var value = prop.GetValue(obj);
@ -1002,6 +1189,23 @@ public static partial class AcBinarySerializer
} }
return; return;
} }
case PropertyAccessorType.String:
{
// Fast path: typed getter, no boxing, no Type.GetTypeCode() call
string? value = prop.GetString(obj);
if (string.IsNullOrEmpty(value))
{
context.WriteByte(value == null ? BinaryTypeCode.PropertySkip : BinaryTypeCode.StringEmpty);
}
else
{
#if DEBUG
context.CurrentPropertyPath = $"{prop.DeclaringType.Name}.{prop.Name}";
#endif
WriteString(value, context);
}
return;
}
default: default:
{ {
// Object type - use regular getter // Object type - use regular getter
@ -1015,6 +1219,9 @@ public static partial class AcBinarySerializer
} }
else else
{ {
#if DEBUG
context.CurrentPropertyPath = $"{prop.DeclaringType.Name}.{prop.Name}";
#endif
WriteValue(value, prop.PropertyType, context, depth); WriteValue(value, prop.PropertyType, context, depth);
} }
return; return;

View File

@ -1,5 +1,4 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using AyCode.Core.Serializers.Jsons;
namespace AyCode.Core.Serializers.Binaries; namespace AyCode.Core.Serializers.Binaries;
@ -23,14 +22,16 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// <summary> /// <summary>
/// Default options instance with metadata and string interning enabled. /// Default options instance with metadata and string interning enabled.
/// Returns a new instance each time to prevent shared state corruption.
/// </summary> /// </summary>
public static readonly AcBinarySerializerOptions Default = new(); public static AcBinarySerializerOptions Default => new();
/// <summary> /// <summary>
/// Options optimized for maximum speed (no interning, no references). /// Options optimized for maximum speed (no interning, no references).
/// Use when deserializer knows the exact type structure. /// Use when deserializer knows the exact type structure.
/// Returns a new instance each time to prevent shared state corruption.
/// </summary> /// </summary>
public static readonly AcBinarySerializerOptions FastMode = new() public static AcBinarySerializerOptions FastMode => new()
{ {
UseStringInterning = false, UseStringInterning = false,
ReferenceHandling = ReferenceHandlingMode.None ReferenceHandling = ReferenceHandlingMode.None
@ -38,8 +39,9 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// <summary> /// <summary>
/// Options for shallow serialization (root level only). /// Options for shallow serialization (root level only).
/// Returns a new instance each time to prevent shared state corruption.
/// </summary> /// </summary>
public static readonly AcBinarySerializerOptions ShallowCopy = new() public static AcBinarySerializerOptions ShallowCopy => new()
{ {
MaxDepth = 0, MaxDepth = 0,
UseStringInterning = false, UseStringInterning = false,
@ -48,8 +50,9 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// <summary> /// <summary>
/// Options optimized for WASM environment with string caching enabled. /// Options optimized for WASM environment with string caching enabled.
/// Returns a new instance each time to prevent shared state corruption.
/// </summary> /// </summary>
public static readonly AcBinarySerializerOptions WasmOptimized = new() public static AcBinarySerializerOptions WasmOptimized => new()
{ {
IsWasm = true, IsWasm = true,
UseStringCaching = true UseStringCaching = true
@ -91,7 +94,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// Reduces size and memory for objects with many repeated string values. /// Reduces size and memory for objects with many repeated string values.
/// Default: true /// Default: true
/// </summary> /// </summary>
public bool UseStringInterning { get; init; } = true; public bool UseStringInterning { get; set; } = true;
/// <summary> /// <summary>
/// Minimum string length to consider for interning. /// Minimum string length to consider for interning.
@ -137,8 +140,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// <summary> /// <summary>
/// Creates options without reference handling (and string interning disabled for speed). /// Creates options without reference handling (and string interning disabled for speed).
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] public static AcBinarySerializerOptions WithoutReferenceHandling => new()
public static AcBinarySerializerOptions WithoutReferenceHandling() => new()
{ {
ReferenceHandling = ReferenceHandlingMode.None, ReferenceHandling = ReferenceHandlingMode.None,
}; };
@ -146,213 +148,5 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// <summary> /// <summary>
/// Creates options without metadata (faster but less flexible). /// Creates options without metadata (faster but less flexible).
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] public static AcBinarySerializerOptions WithoutMetadata => new() { UseMetadata = false };
public static AcBinarySerializerOptions WithoutMetadata() => new() { UseMetadata = false }; }
}
/// <summary>
/// Binary type codes for serialization.
/// Designed for fast switch dispatch and compact storage.
/// Lower 5 bits = type code (0-31)
/// Upper 3 bits = flags (interned, reference, has-type-info)
/// </summary>
internal static class BinaryTypeCode
{
// Primitive types (0-15)
public const byte Null = 0;
public const byte True = 1;
public const byte False = 2;
public const byte Int8 = 3;
public const byte UInt8 = 4;
public const byte Int16 = 5;
public const byte UInt16 = 6;
public const byte Int32 = 7;
public const byte UInt32 = 8;
public const byte Int64 = 9;
public const byte UInt64 = 10;
public const byte Float32 = 11;
public const byte Float64 = 12;
public const byte Decimal = 13;
public const byte Char = 14;
// String types (16-19)
public const byte String = 16; // Inline UTF8 string
public const byte StringInterned = 17; // Reference to interned string by index
public const byte StringEmpty = 18; // Empty string marker
public const byte StringInternNew = 19; // New interned string - full content + register in table
// Date/Time types (20-23)
public const byte DateTime = 20;
public const byte DateTimeOffset = 21;
public const byte TimeSpan = 22;
public const byte Guid = 23;
// Enum (24)
public const byte Enum = 24;
// Complex types (25-31)
public const byte Object = 25; // Start of object
public const byte ObjectEnd = 26; // End of object marker
public const byte ObjectRef = 27; // Reference to previously serialized object
public const byte Array = 28; // Start of array/list
public const byte Dictionary = 29; // Start of dictionary
public const byte ByteArray = 30; // Optimized byte[] storage
// Special markers (32+, for header/meta)
// Header flags byte structure (for values >= 64):
// Bit 0 (0x01): HasMetadata
// Bit 1 (0x02): HasReferenceHandling
// Values 32, 33 are legacy for backward compatibility
public const byte MetadataHeader = 32; // Binary has metadata section (legacy, implies HasReferenceHandling=true)
public const byte NoMetadataHeader = 33; // Binary has no metadata (legacy, implies HasReferenceHandling=true)
// FixStr range: 34-65 (32 values for strings 0-31 bytes)
// FixStr encoding: FixStrBase + length (0-31)
// This saves 1 byte for short strings by combining type + length in single byte
public const byte FixStrBase = 34; // Base value for FixStr (0xA0 in MessagePack style, but we use 34)
public const byte FixStrMax = 65; // FixStrBase + 31 = maximum FixStr code
// New flag-based header markers (48+) - moved to after FixStr range
// Note: FixStr range 34-65 overlaps with old HeaderFlagsBase, so headers use version byte prefix
// Header byte structure: (marker & 0xF0) == HeaderFlagsBase, flags in (marker & 0x0F)
public const byte HeaderFlagsBase = 48; // Base value for flag-based headers (0x30)
public const byte HeaderFlag_Metadata = 0x01; // Bit 0: property metadata included
// Reference handling uses 2 separate bits:
// Bit 1 (0x02): OnlyId - reference handling for IId objects only
// Bit 2 (0x04): All - reference handling for all objects (includes OnlyId)
// None = both false, OnlyId = 0x02, All = 0x06 (both bits set)
public const byte HeaderFlag_RefHandling_OnlyId = 0x02;
public const byte HeaderFlag_RefHandling_All = 0x04;
public const byte HeaderFlag_HasFooterPosition = 0x08; // Bit 3: 4-byte footer position follows flags
// Compact integer variants (for VarInt optimization)
public const byte Int32Tiny = 192; // -16 to 63 stored in single byte (value = code - 192 - 16)
public const byte Int32TinyMax = 255; // Upper bound for tiny int (192 + 64 - 1 = 255)
// Property skip marker (for single-pass serialization optimization)
// CRITICAL: Must be in the "reserved" range 67-191 (after FixStr, before TinyInt)
// AND must not conflict with any other type codes.
// Using 191 (0xBF) - the highest value before TinyInt range starts at 192.
// This ensures it won't be confused with:
// - Primitive types (0-31)
// - FixStr (34-65)
// - TinyInt values (192-255)
public const byte PropertySkip = 191; // Marks a property with default/null value (skipped during serialization)
/// <summary>
/// Check if type code represents a reference (string or object).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsReference(byte code) => code is StringInterned or ObjectRef;
/// <summary>
/// Check if type code is a FixStr (short string with length encoded in type code).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsFixStr(byte code) => code is >= FixStrBase and <= FixStrMax;
/// <summary>
/// Decode FixStr length from type code.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int DecodeFixStrLength(byte code) => code - FixStrBase;
/// <summary>
/// Encode FixStr type code for given byte length (0-31).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte EncodeFixStr(int byteLength) => (byte)(FixStrBase + byteLength);
/// <summary>
/// Check if byte length can be encoded as FixStr.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool CanEncodeAsFixStr(int byteLength) => byteLength is >= 0 and <= 31;
/// <summary>
/// Check if type code is a tiny int (single byte int32 encoding).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsTinyInt(byte code) => code >= Int32Tiny;
/// <summary>
/// Decode tiny int value from type code.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int DecodeTinyInt(byte code) => code - Int32Tiny - 16;
/// <summary>
/// Encode small int value (-16 to 47) as type code.
/// Returns true if value fits in tiny encoding.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryEncodeTinyInt(int value, out byte code)
{
// Range: -16 to 47 (64 values total, fitting in 192-255)
if (value is >= -16 and <= 47)
{
code = (byte)(value + 16 + Int32Tiny);
return true;
}
code = 0;
return false;
}
}
/// <summary>
/// Delegate used to decide whether a property should be serialized.
/// </summary>
public delegate bool BinaryPropertyFilter(in BinaryPropertyFilterContext context);
/// <summary>
/// Provides property metadata and lazy value access for property filter evaluations.
/// </summary>
public readonly struct BinaryPropertyFilterContext
{
private readonly object? _instance;
private readonly Func<object, object?>? _valueGetter;
internal BinaryPropertyFilterContext(object? instance, Type declaringType, string propertyName, Type propertyType, Func<object, object?>? valueGetter)
{
_instance = instance;
DeclaringType = declaringType;
PropertyName = propertyName;
PropertyType = propertyType;
_valueGetter = valueGetter;
}
/// <summary>
/// Gets the declaring type of the property.
/// </summary>
public Type DeclaringType { get; }
/// <summary>
/// Gets the property name.
/// </summary>
public string PropertyName { get; }
/// <summary>
/// Gets the property type.
/// </summary>
public Type PropertyType { get; }
/// <summary>
/// Gets the instance being serialized when available. Null during metadata registration.
/// </summary>
public object? Instance => _instance;
/// <summary>
/// Indicates whether the filter is invoked during metadata registration (when no instance is available).
/// </summary>
public bool IsMetadataPhase => _instance is null;
/// <summary>
/// Lazily obtains the current property value. Returns null when invoked during metadata registration.
/// </summary>
public object? GetValue()
{
if (_instance == null || _valueGetter == null)
return null;
return _valueGetter(_instance);
}
}

View File

@ -0,0 +1,60 @@
namespace AyCode.Core.Serializers.Binaries;
/// <summary>
/// Delegate used to decide whether a property should be serialized.
/// </summary>
public delegate bool BinaryPropertyFilter(in BinaryPropertyFilterContext context);
/// <summary>
/// Provides property metadata and lazy value access for property filter evaluations.
/// </summary>
public readonly struct BinaryPropertyFilterContext
{
private readonly object? _instance;
private readonly Func<object, object?>? _valueGetter;
internal BinaryPropertyFilterContext(object? instance, Type declaringType, string propertyName, Type propertyType, Func<object, object?>? valueGetter)
{
_instance = instance;
DeclaringType = declaringType;
PropertyName = propertyName;
PropertyType = propertyType;
_valueGetter = valueGetter;
}
/// <summary>
/// Gets the declaring type of the property.
/// </summary>
public Type DeclaringType { get; }
/// <summary>
/// Gets the property name.
/// </summary>
public string PropertyName { get; }
/// <summary>
/// Gets the property type.
/// </summary>
public Type PropertyType { get; }
/// <summary>
/// Gets the instance being serialized when available. Null during metadata registration.
/// </summary>
public object? Instance => _instance;
/// <summary>
/// Indicates whether the filter is invoked during metadata registration (when no instance is available).
/// </summary>
public bool IsMetadataPhase => _instance is null;
/// <summary>
/// Lazily obtains the current property value. Returns null when invoked during metadata registration.
/// </summary>
public object? GetValue()
{
if (_instance == null || _valueGetter == null)
return null;
return _valueGetter(_instance);
}
}

View File

@ -0,0 +1,153 @@
using System.Runtime.CompilerServices;
namespace AyCode.Core.Serializers.Binaries;
/// <summary>
/// Binary type codes for serialization.
/// Designed for fast switch dispatch and compact storage.
/// Lower 5 bits = type code (0-31)
/// Upper 3 bits = flags (interned, reference, has-type-info)
/// </summary>
internal static class BinaryTypeCode
{
// Primitive types (0-15)
public const byte Null = 0;
public const byte True = 1;
public const byte False = 2;
public const byte Int8 = 3;
public const byte UInt8 = 4;
public const byte Int16 = 5;
public const byte UInt16 = 6;
public const byte Int32 = 7;
public const byte UInt32 = 8;
public const byte Int64 = 9;
public const byte UInt64 = 10;
public const byte Float32 = 11;
public const byte Float64 = 12;
public const byte Decimal = 13;
public const byte Char = 14;
// String types (16-19)
public const byte String = 16; // Inline UTF8 string
public const byte StringInterned = 17; // Reference to interned string by index
public const byte StringEmpty = 18; // Empty string marker
public const byte StringInternNew = 19; // New interned string - full content + register in table
// Date/Time types (20-23)
public const byte DateTime = 20;
public const byte DateTimeOffset = 21;
public const byte TimeSpan = 22;
public const byte Guid = 23;
// Enum (24)
public const byte Enum = 24;
// Complex types (25-31)
public const byte Object = 25; // Start of object
public const byte ObjectEnd = 26; // End of object marker
public const byte ObjectRef = 27; // Reference to previously serialized object
public const byte Array = 28; // Start of array/list
public const byte Dictionary = 29; // Start of dictionary
public const byte ByteArray = 30; // Optimized byte[] storage
// Special markers (32+, for header/meta)
// Header flags byte structure (for values >= 64):
// Bit 0 (0x01): HasMetadata
// Bit 1 (0x02): HasReferenceHandling
// Values 32, 33 are legacy for backward compatibility
public const byte MetadataHeader = 32; // Binary has metadata section (legacy, implies HasReferenceHandling=true)
public const byte NoMetadataHeader = 33; // Binary has no metadata (legacy, implies HasReferenceHandling=true)
// FixStr range: 34-65 (32 values for strings 0-31 bytes)
// FixStr encoding: FixStrBase + length (0-31)
// This saves 1 byte for short strings by combining type + length in single byte
public const byte FixStrBase = 34; // Base value for FixStr (0xA0 in MessagePack style, but we use 34)
public const byte FixStrMax = 65; // FixStrBase + 31 = maximum FixStr code
public const int FixStrMaxLength = 31; // Maximum string length encodable as FixStr
// New flag-based header markers (48+) - moved to after FixStr range
// Note: FixStr range 34-65 overlaps with old HeaderFlagsBase, so headers use version byte prefix
// Header byte structure: (marker & 0xF0) == HeaderFlagsBase, flags in (marker & 0x0F)
public const byte HeaderFlagsBase = 48; // Base value for flag-based headers (0x30)
public const byte HeaderFlag_Metadata = 0x01; // Bit 0: property metadata included
// Reference handling uses 2 separate bits:
// Bit 1 (0x02): OnlyId - reference handling for IId objects only
// Bit 2 (0x04): All - reference handling for all objects (includes OnlyId)
// None = both false, OnlyId = 0x02, All = 0x06 (both bits set)
public const byte HeaderFlag_RefHandling_OnlyId = 0x02;
public const byte HeaderFlag_RefHandling_All = 0x04;
public const byte HeaderFlag_HasFooterPosition = 0x08; // Bit 3: 4-byte footer position follows flags
// Compact integer variants (for VarInt optimization)
public const byte Int32Tiny = 192; // -16 to 63 stored in single byte (value = code - 192 - 16)
public const byte Int32TinyMax = 255; // Upper bound for tiny int (192 + 64 - 1 = 255)
// Property skip marker (for single-pass serialization optimization)
// CRITICAL: Must be in the "reserved" range 67-191 (after FixStr, before TinyInt)
// AND must not conflict with any other type codes.
// Using 191 (0xBF) - the highest value before TinyInt range starts at 192.
// This ensures it won't be confused with:
// - Primitive types (0-31)
// - FixStr (34-65)
// - TinyInt values (192-255)
public const byte PropertySkip = 191; // Marks a property with default/null value (skipped during serialization)
/// <summary>
/// Check if type code represents a reference (string or object).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsReference(byte code) => code is StringInterned or ObjectRef;
/// <summary>
/// Check if type code is a FixStr (short string with length encoded in type code).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsFixStr(byte code) => code is >= FixStrBase and <= FixStrMax;
/// <summary>
/// Decode FixStr length from type code.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int DecodeFixStrLength(byte code) => code - FixStrBase;
/// <summary>
/// Encode FixStr type code for given byte length (0-31).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte EncodeFixStr(int byteLength) => (byte)(FixStrBase + byteLength);
/// <summary>
/// Check if byte length can be encoded as FixStr.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool CanEncodeAsFixStr(int byteLength) => byteLength is >= 0 and <= 31;
/// <summary>
/// Check if type code is a tiny int (single byte int32 encoding).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsTinyInt(byte code) => code >= Int32Tiny;
/// <summary>
/// Decode tiny int value from type code.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int DecodeTinyInt(byte code) => code - Int32Tiny - 16;
/// <summary>
/// Encode small int value (-16 to 47) as type code.
/// Returns true if value fits in tiny encoding.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryEncodeTinyInt(int value, out byte code)
{
// Range: -16 to 47 (64 values total, fitting in 192-255)
if (value is >= -16 and <= 47)
{
code = (byte)(value + 16 + Int32Tiny);
return true;
}
code = 0;
return false;
}
}

View File

@ -1,5 +1,4 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using AyCode.Core.Serializers.Jsons;
namespace AyCode.Core.Serializers; namespace AyCode.Core.Serializers;

View File

@ -1,92 +1,7 @@
using System.Reflection; using System.Reflection;
namespace AyCode.Core.Serializers.Jsons; namespace AyCode.Core.Serializers.Jsons;
public enum AcSerializerType : byte
{
Json = 0,
Binary = 1,
Toon = 2,
}
/// <summary>
/// Reference handling mode for serialization.
/// </summary>
public enum ReferenceHandlingMode : byte
{
/// <summary>
/// No reference handling - all objects serialized inline.
/// </summary>
None = 0,
/// <summary>
/// Reference handling only for IId objects - uses semantic Id for deduplication.
/// NOTE: Not fully implemented for JSON serializer - use All instead.
/// Binary serializer supports this mode.
/// </summary>
OnlyId = 1,
/// <summary>
/// Full reference handling for all objects.
/// </summary>
All = 2
}
/// <summary>
/// Delegate for custom property mapping during cross-type deserialization/population.
/// Enables mapping between different class hierarchies or renamed properties.
/// </summary>
/// <param name="sourceProperty">Property from the source type being deserialized</param>
/// <param name="destinationType">Target type being populated</param>
/// <returns>Mapped destination property, or null to skip this property</returns>
public delegate PropertyInfo? PropertyMapperDelegate(PropertyInfo sourceProperty, Type destinationType);
public abstract class AcSerializerOptions
{
public abstract AcSerializerType SerializerType { get; init; }
/// <summary>
/// Reference handling mode for circular/shared references.
/// Default: OnlyId (JSON serializer requires All mode, OnlyId not yet implemented)
/// Note: Binary serializer supports OnlyId mode for IId-only tracking.
/// </summary>
public ReferenceHandlingMode ReferenceHandling { get; set; } = ReferenceHandlingMode.OnlyId;
/// <summary>
/// Maximum depth for serialization/deserialization.
/// 0 = root level only (primitives of root object)
/// 1 = root + first level of nested objects/collections
/// byte.MaxValue (255) = effectively unlimited
/// Default: byte.MaxValue
/// </summary>
public byte MaxDepth { get; init; } = byte.MaxValue;
/// <summary>
/// Throw exception on circular reference detection for non-IId types.
/// When true: Tracks all objects and throws InvalidOperationException on circular references.
/// When false: No tracking for non-IId types (faster, but circular refs may cause MaxDepth truncation).
/// Default: true (production safety)
/// Note: IId types are always tracked when ReferenceHandling != None.
/// </summary>
public bool ThrowOnCircularReference { get; init; } = true;
/// <summary>
/// Optional callback for custom property mapping during cross-type operations.
/// Used when deserializing/populating with Deserialize&lt;TSource, TDest&gt; or Populate&lt;TSource, TDest&gt;.
///
/// Use cases:
/// - Mapping between external DTOs and internal models (different class hierarchies)
/// - Handling property renames across versions
/// - Custom property pairing logic
///
/// If null (default), properties are matched by name.
/// Callback is invoked once during mapping build phase and result is cached.
///
/// Performance: ZERO overhead on same-type operations (Deserialize&lt;T&gt;).
/// </summary>
public PropertyMapperDelegate? PropertyMapper { get; init; }
}
/// <summary> /// <summary>
/// Options for AcJsonSerializer and AcJsonDeserializer. /// Options for AcJsonSerializer and AcJsonDeserializer.
/// </summary> /// </summary>
@ -96,13 +11,15 @@ public sealed class AcJsonSerializerOptions : AcSerializerOptions
/// <summary> /// <summary>
/// Default options instance with reference handling enabled and max depth. /// Default options instance with reference handling enabled and max depth.
/// Returns a new instance each time to prevent shared state corruption.
/// </summary> /// </summary>
public static readonly AcJsonSerializerOptions Default = new() { ReferenceHandling = ReferenceHandlingMode.All }; public static AcJsonSerializerOptions Default => new() { ReferenceHandling = ReferenceHandlingMode.All };
/// <summary> /// <summary>
/// Options for shallow serialization (root level only, no references). /// Options for shallow serialization (root level only, no references).
/// Returns a new instance each time to prevent shared state corruption.
/// </summary> /// </summary>
public static readonly AcJsonSerializerOptions ShallowCopy = new() { MaxDepth = 0, ReferenceHandling = ReferenceHandlingMode.None }; public static AcJsonSerializerOptions ShallowCopy => new() { MaxDepth = 0, ReferenceHandling = ReferenceHandlingMode.None };
/// <summary> /// <summary>
/// Creates options with specified max depth. /// Creates options with specified max depth.
@ -112,5 +29,5 @@ public sealed class AcJsonSerializerOptions : AcSerializerOptions
/// <summary> /// <summary>
/// Creates options without reference handling. /// Creates options without reference handling.
/// </summary> /// </summary>
public static AcJsonSerializerOptions WithoutReferenceHandling() => new() { ReferenceHandling = ReferenceHandlingMode.None }; public static AcJsonSerializerOptions WithoutReferenceHandling => new() { ReferenceHandling = ReferenceHandlingMode.None };
} }

View File

@ -27,6 +27,7 @@ public abstract class PropertyAccessorBase : PropertyMetadataBase
private readonly Func<object, uint>? _uint32Getter; private readonly Func<object, uint>? _uint32Getter;
private readonly Func<object, ulong>? _uint64Getter; private readonly Func<object, ulong>? _uint64Getter;
private readonly Func<object, Guid>? _guidGetter; private readonly Func<object, Guid>? _guidGetter;
private readonly Func<object, string?>? _stringGetter;
#endregion #endregion
@ -82,6 +83,9 @@ public abstract class PropertyAccessorBase : PropertyMetadataBase
case PropertyAccessorType.Enum: case PropertyAccessorType.Enum:
Unsafe.AsRef(in _int32Getter) = AcSerializerCommon.CreateEnumGetter(declaringType, prop); Unsafe.AsRef(in _int32Getter) = AcSerializerCommon.CreateEnumGetter(declaringType, prop);
break; break;
case PropertyAccessorType.String:
Unsafe.AsRef(in _stringGetter) = AcSerializerCommon.CreateTypedGetter<string?>(declaringType, prop);
break;
} }
} }
@ -129,5 +133,8 @@ public abstract class PropertyAccessorBase : PropertyMetadataBase
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetEnumAsInt32(object obj) => _int32Getter!(obj); public int GetEnumAsInt32(object obj) => _int32Getter!(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string? GetString(object obj) => _stringGetter!(obj);
#endregion #endregion
} }

View File

@ -24,7 +24,8 @@ public enum PropertyAccessorType : byte
UInt32, UInt32,
UInt64, UInt64,
Guid, Guid,
Enum Enum,
String
} }
/// <summary> /// <summary>
@ -137,6 +138,7 @@ public abstract class PropertyMetadataBase
TypeCode.UInt16 => PropertyAccessorType.UInt16, TypeCode.UInt16 => PropertyAccessorType.UInt16,
TypeCode.UInt32 => PropertyAccessorType.UInt32, TypeCode.UInt32 => PropertyAccessorType.UInt32,
TypeCode.UInt64 => PropertyAccessorType.UInt64, TypeCode.UInt64 => PropertyAccessorType.UInt64,
TypeCode.String => PropertyAccessorType.String,
_ => PropertyAccessorType.Object _ => PropertyAccessorType.Object
}; };
} }

View File

@ -1,4 +1,3 @@
using AyCode.Core.Serializers.Jsons;
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;

View File

@ -3,7 +3,6 @@ using System.Collections;
using System.Globalization; using System.Globalization;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using AyCode.Core.Serializers.Jsons;
using static AyCode.Core.Helpers.JsonUtilities; using static AyCode.Core.Helpers.JsonUtilities;
namespace AyCode.Core.Serializers.Toons; namespace AyCode.Core.Serializers.Toons;

View File

@ -4,7 +4,6 @@ using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using AyCode.Core.Serializers.Jsons;
//using ReferenceEqualityComparer = AyCode.Core.Serializers.ReferenceEqualityComparer; //using ReferenceEqualityComparer = AyCode.Core.Serializers.ReferenceEqualityComparer;

View File

@ -3,7 +3,6 @@ using System.Collections.Concurrent;
using System.Globalization; using System.Globalization;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using AyCode.Core.Serializers.Jsons;
using static AyCode.Core.Helpers.JsonUtilities; using static AyCode.Core.Helpers.JsonUtilities;
namespace AyCode.Core.Serializers.Toons; namespace AyCode.Core.Serializers.Toons;

View File

@ -1,5 +1,3 @@
using AyCode.Core.Serializers.Jsons;
namespace AyCode.Core.Serializers.Toons; namespace AyCode.Core.Serializers.Toons;
/// <summary> /// <summary>
@ -136,8 +134,9 @@ public sealed class AcToonSerializerOptions : AcSerializerOptions
/// <summary> /// <summary>
/// Full mode: Meta + Data (first-time serialization). /// Full mode: Meta + Data (first-time serialization).
/// Use when LLM needs complete context about data structure and values. /// Use when LLM needs complete context about data structure and values.
/// Returns a new instance each time to prevent shared state corruption.
/// </summary> /// </summary>
public static readonly AcToonSerializerOptions Default = new() public static AcToonSerializerOptions Default => new()
{ {
Mode = ToonSerializationMode.Full, Mode = ToonSerializationMode.Full,
UseMeta = true, UseMeta = true,
@ -150,8 +149,9 @@ public sealed class AcToonSerializerOptions : AcSerializerOptions
/// Meta-only mode: Only serialize type definitions and descriptions. /// Meta-only mode: Only serialize type definitions and descriptions.
/// Use this to send schema information once at conversation start. /// Use this to send schema information once at conversation start.
/// Subsequent serializations can use DataOnly mode to save tokens. /// Subsequent serializations can use DataOnly mode to save tokens.
/// Returns a new instance each time to prevent shared state corruption.
/// </summary> /// </summary>
public static readonly AcToonSerializerOptions MetaOnly = new() public static AcToonSerializerOptions MetaOnly => new()
{ {
Mode = ToonSerializationMode.MetaOnly, Mode = ToonSerializationMode.MetaOnly,
UseMeta = true, UseMeta = true,
@ -163,8 +163,9 @@ public sealed class AcToonSerializerOptions : AcSerializerOptions
/// Data-only mode: Only serialize actual data values. /// Data-only mode: Only serialize actual data values.
/// Use this when schema was already sent via MetaOnly. /// Use this when schema was already sent via MetaOnly.
/// Saves ~30-50% tokens in repeated serializations. /// Saves ~30-50% tokens in repeated serializations.
/// Returns a new instance each time to prevent shared state corruption.
/// </summary> /// </summary>
public static readonly AcToonSerializerOptions DataOnly = new() public static AcToonSerializerOptions DataOnly => new()
{ {
Mode = ToonSerializationMode.DataOnly, Mode = ToonSerializationMode.DataOnly,
UseMeta = false, UseMeta = false,
@ -176,8 +177,9 @@ public sealed class AcToonSerializerOptions : AcSerializerOptions
/// <summary> /// <summary>
/// Compact mode: Minimal output, no meta, no indentation. /// Compact mode: Minimal output, no meta, no indentation.
/// Maximum token efficiency but less readable. /// Maximum token efficiency but less readable.
/// Returns a new instance each time to prevent shared state corruption.
/// </summary> /// </summary>
public static readonly AcToonSerializerOptions Compact = new() public static AcToonSerializerOptions Compact => new()
{ {
Mode = ToonSerializationMode.DataOnly, Mode = ToonSerializationMode.DataOnly,
UseMeta = false, UseMeta = false,
@ -190,8 +192,9 @@ public sealed class AcToonSerializerOptions : AcSerializerOptions
/// <summary> /// <summary>
/// Verbose mode: Everything included (for debugging/documentation). /// Verbose mode: Everything included (for debugging/documentation).
/// Use when you need maximum information and clarity. /// Use when you need maximum information and clarity.
/// Returns a new instance each time to prevent shared state corruption.
/// </summary> /// </summary>
public static readonly AcToonSerializerOptions Verbose = new() public static AcToonSerializerOptions Verbose => new()
{ {
Mode = ToonSerializationMode.Full, Mode = ToonSerializationMode.Full,
UseMeta = true, UseMeta = true,

View File

@ -1,6 +1,5 @@
using AyCode.Core.Helpers; using AyCode.Core.Helpers;
using AyCode.Core.Interfaces; using AyCode.Core.Interfaces;
using AyCode.Core.Serializers.Jsons;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;

View File

@ -1,7 +1,6 @@
using System; using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using AyCode.Core.Helpers; using AyCode.Core.Helpers;
using AyCode.Core.Serializers.Jsons;
namespace AyCode.Core.Serializers; namespace AyCode.Core.Serializers;

View File

@ -6,6 +6,7 @@ using AyCode.Services.Server.SignalRs;
using AyCode.Services.SignalRs; using AyCode.Services.SignalRs;
using MessagePack.Resolvers; using MessagePack.Resolvers;
using AyCode.Core.Tests.Serialization; using AyCode.Core.Tests.Serialization;
using AyCode.Core.Serializers;
namespace AyCode.Services.Server.Tests.SignalRs; namespace AyCode.Services.Server.Tests.SignalRs;

View File

@ -1,4 +1,4 @@
using AyCode.Core.Serializers.Jsons; using AyCode.Core.Serializers;
using AyCode.Core.Tests.TestModels; using AyCode.Core.Tests.TestModels;
using AyCode.Services.Server.SignalRs; using AyCode.Services.Server.SignalRs;
using AyCode.Services.SignalRs; using AyCode.Services.SignalRs;

View File

@ -1,5 +1,5 @@
using AyCode.Core.Serializers;
using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Serializers.Jsons;
using AyCode.Core.Tests.TestModels; using AyCode.Core.Tests.TestModels;
using AyCode.Services.SignalRs; using AyCode.Services.SignalRs;

View File

@ -1,5 +1,5 @@
using AyCode.Core.Serializers;
using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Serializers.Jsons;
using AyCode.Core.Tests.TestModels; using AyCode.Core.Tests.TestModels;
using AyCode.Services.SignalRs; using AyCode.Services.SignalRs;

View File

@ -1,3 +1,4 @@
using AyCode.Core.Serializers;
using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Serializers.Jsons; using AyCode.Core.Serializers.Jsons;
using AyCode.Core.Tests.TestModels; using AyCode.Core.Tests.TestModels;

View File

@ -1,6 +1,6 @@
using AyCode.Core.Helpers; using AyCode.Core.Helpers;
using AyCode.Core.Serializers;
using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Serializers.Jsons;
using AyCode.Core.Tests.TestModels; using AyCode.Core.Tests.TestModels;
using AyCode.Services.SignalRs; using AyCode.Services.SignalRs;

View File

@ -1,4 +1,5 @@
using AyCode.Core.Helpers; using AyCode.Core.Helpers;
using AyCode.Core.Serializers;
using AyCode.Core.Serializers.Jsons; using AyCode.Core.Serializers.Jsons;
using AyCode.Core.Tests.TestModels; using AyCode.Core.Tests.TestModels;
using AyCode.Services.SignalRs; using AyCode.Services.SignalRs;

View File

@ -1,7 +1,7 @@
using System.Security.Claims; using System.Security.Claims;
using AyCode.Core.Extensions; using AyCode.Core.Extensions;
using AyCode.Core.Helpers; using AyCode.Core.Helpers;
using AyCode.Core.Serializers.Jsons; using AyCode.Core.Serializers;
using AyCode.Core.Tests.TestModels; using AyCode.Core.Tests.TestModels;
using AyCode.Models.Server.DynamicMethods; using AyCode.Models.Server.DynamicMethods;
using AyCode.Services.Server.SignalRs; using AyCode.Services.Server.SignalRs;

View File

@ -7,8 +7,8 @@ using System.Collections;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using AyCode.Core.Serializers.Jsons;
using AyCode.Core.Compression; using AyCode.Core.Compression;
using AyCode.Core.Serializers;
namespace AyCode.Services.Server.SignalRs namespace AyCode.Services.Server.SignalRs
{ {

View File

@ -4,6 +4,7 @@ using AyCode.Core;
using AyCode.Core.Extensions; using AyCode.Core.Extensions;
using AyCode.Core.Helpers; using AyCode.Core.Helpers;
using AyCode.Core.Loggers; using AyCode.Core.Loggers;
using AyCode.Core.Serializers;
using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Serializers.Jsons; using AyCode.Core.Serializers.Jsons;
using AyCode.Models.Server.DynamicMethods; using AyCode.Models.Server.DynamicMethods;

View File

@ -3,7 +3,7 @@ using AyCode.Core;
using AyCode.Core.Extensions; using AyCode.Core.Extensions;
using AyCode.Core.Helpers; using AyCode.Core.Helpers;
using AyCode.Core.Loggers; using AyCode.Core.Loggers;
using AyCode.Core.Serializers.Jsons; using AyCode.Core.Serializers;
using AyCode.Interfaces.Entities; using AyCode.Interfaces.Entities;
using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.SignalR.Client; using Microsoft.AspNetCore.SignalR.Client;

View File

@ -5,6 +5,7 @@ using System.Runtime.CompilerServices;
using AyCode.Core.Serializers.Jsons; using AyCode.Core.Serializers.Jsons;
using JsonIgnoreAttribute = Newtonsoft.Json.JsonIgnoreAttribute; using JsonIgnoreAttribute = Newtonsoft.Json.JsonIgnoreAttribute;
using STJIgnore = System.Text.Json.Serialization.JsonIgnoreAttribute; using STJIgnore = System.Text.Json.Serialization.JsonIgnoreAttribute;
using AyCode.Core.Serializers;
namespace AyCode.Services.SignalRs; namespace AyCode.Services.SignalRs;

View File

@ -2,6 +2,7 @@ using System.Buffers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using AyCode.Core.Compression; using AyCode.Core.Compression;
using AyCode.Core.Extensions; using AyCode.Core.Extensions;
using AyCode.Core.Serializers;
using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Serializers.Jsons; using AyCode.Core.Serializers.Jsons;