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

View File

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

View File

@ -83,7 +83,7 @@ public class PureContractlessBenchmark
Status = "Available"
};
_binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
_binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
_msgPackContractlessOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
_acBinaryData = AcBinarySerializer.Serialize(_testData, _binaryOptions);
@ -148,7 +148,7 @@ public class SourceGeneratorVsRuntimeBenchmark
Status = "Available"
};
_binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
_binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
// MessagePack with Source Generator (uses [MessagePackObject] + [Key] attributes)
_msgPackOptions = MessagePackSerializerOptions.Standard;
@ -254,7 +254,7 @@ public class RepeatedStringBenchmark
Type = i % 4 == 0 ? "TypeA" : i % 4 == 1 ? "TypeB" : i % 4 == 2 ? "TypeC" : "TypeD"
}).ToList();
_binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
_binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
_msgPackOptions = MessagePackSerializerOptions.Standard;
_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 int WarmupIterations = 5;
private static int WarmupIterations = 2000;
private static int TestIterations = 1000;
public static void Main(string[] args)
@ -90,8 +90,6 @@ public static class Program
// Print grouped results
PrintGroupedResults(allResults, testDataSets);
// Save results to file
SaveResults(allResults, testDataSets);
@ -122,7 +120,8 @@ public static class Program
sharedTag: sharedTag,
sharedUser: sharedUser);
var options = AcBinarySerializerOptions.WithoutReferenceHandling();
var options = AcBinarySerializerOptions.WithoutReferenceHandling;
options.UseStringInterning = false;
// Warmup (fills caches)
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();
// 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("Press any key to exit...");
System.Console.ReadKey(intercept: true);
@ -369,6 +378,9 @@ public static class Program
serializer.Warmup(WarmupIterations);
}
// Wait for tiered JIT background compilation to complete
Thread.Sleep(2000);
// Run benchmarks
System.Console.WriteLine($"Running benchmarks ({TestIterations} iterations)...\n");
@ -402,9 +414,10 @@ public static class Program
{
return new List<ISerializerBenchmark>
{
// AcBinary variants
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, new AcBinarySerializerOptions { UseStringInterning = false }, SerializerAcBinaryNoIntern),

View File

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

View File

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

View File

@ -109,7 +109,7 @@ public class GeneratedSerializerIntegrationTests
};
// 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);
// Assert

View File

@ -1,5 +1,6 @@
using System.Diagnostics;
using AyCode.Core.Extensions;
using AyCode.Core.Serializers;
using AyCode.Core.Serializers.Attributes;
using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Serializers.Jsons;
@ -12,7 +13,7 @@ namespace AyCode.Core.Tests.Serialization;
[TestClass]
public class QuickBenchmark
{
private static readonly MessagePackSerializerOptions MsgPackOptions =
private static readonly MessagePackSerializerOptions MsgPackOptions =
ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
private const int DefaultIterations = 1000;
@ -129,7 +130,7 @@ public class QuickBenchmark
var deserializeMs = sw.Elapsed.TotalMilliseconds;
// JSON comparison
var jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling();
var jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling;
sw.Restart();
string json = null!;
for (int i = 0; i < iterations; i++)
@ -184,7 +185,7 @@ public class QuickBenchmark
}
const int iterations = DefaultIterations;
// With interning (default)
var sw = Stopwatch.StartNew();
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($"{"Round-trip (ms)",-25} {acBinarySerMs + acBinaryDeserMs,12:F2} {msgPackSerMs + msgPackDeserMs,12:F2} {(acBinarySerMs + acBinaryDeserMs) / (msgPackSerMs + msgPackDeserMs),9:F2}x");
Console.WriteLine();
var sizeDiff = msgPackData.Length - acBinaryData.Length;
if (sizeDiff > 0)
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)
#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]
public void RunFullBenchmarkComparison()
{
@ -397,23 +506,32 @@ public class QuickBenchmark
sharedMetadata: sharedMeta);
// Options
var withRefOptions = new AcBinarySerializerOptions();
var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
// Warmup
Console.WriteLine("\nWarming up...");
for (int i = 0; i < 10; i++)
{
_ = AcBinarySerializer.Serialize(testOrder, withRefOptions);
_ = AcBinarySerializer.Serialize(testOrder, noRefOptions);
_ = MessagePackSerializer.Serialize(testOrder, MsgPackOptions);
}
var withRefOptions = AcBinarySerializerOptions.Default;
//withRefOptions.UseStringInterning = false;
var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
noRefOptions.UseStringInterning = false;
// Pre-serialize
var acBinaryWithRef = AcBinarySerializer.Serialize(testOrder, withRefOptions);
var acBinaryNoRef = AcBinarySerializer.Serialize(testOrder, noRefOptions);
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}");
// Size comparison
@ -519,7 +637,7 @@ public class QuickBenchmark
sharedMetadata: sharedMeta);
var withRefOptions = new AcBinarySerializerOptions();
var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
// Warmup
Console.WriteLine("Warming up...");
@ -602,7 +720,7 @@ public class QuickBenchmark
measurementsPerPallet: 3,
pointsPerMeasurement: 4);
var options = AcBinarySerializerOptions.WithoutReferenceHandling();
var options = AcBinarySerializerOptions.WithoutReferenceHandling;
var binaryData = AcBinarySerializer.Serialize(testOrder, options);
Console.WriteLine("Warming up...");

View File

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

View File

@ -4,7 +4,6 @@ using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using AyCode.Core.Serializers.Expressions;
using AyCode.Core.Serializers.Jsons;
using LExpression = System.Linq.Expressions.Expression;
using LExpressionType = System.Linq.Expressions.ExpressionType;
@ -538,8 +537,13 @@ public static class AcSerializerCommon
var objParam = LExpression.Parameter(typeof(object), "obj");
var castExpr = LExpression.Convert(objParam, declaringType);
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>

View File

@ -1,4 +1,3 @@
using AyCode.Core.Serializers.Jsons;
using System;
using System.Collections.Concurrent;
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.Collections.Generic;
using System.Runtime.CompilerServices;

View File

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

View File

@ -1,4 +1,3 @@
using AyCode.Core.Serializers.Jsons;
using System;
using System.Buffers;
using System.Collections.Concurrent;
@ -77,6 +76,21 @@ public static partial class AcBinarySerializer
private int[]? _propertyIndexBuffer;
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
public bool UseStringInterning => Options.UseStringInterning;
public bool UseMetadata => Options.UseMetadata;
@ -1161,6 +1175,48 @@ public static partial class AcBinarySerializer
_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>
/// Write short UTF8 bytes using FixStr encoding.
/// 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 System.Buffers;
using System.Collections;
@ -44,6 +44,177 @@ public static partial class AcBinarySerializer
#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>
/// Serialize object to binary with default options.
/// </summary>
@ -607,19 +778,25 @@ public static partial class AcBinarySerializer
&& (context.MaxStringInternLength == 0 || value.Length <= context.MaxStringInternLength))
{
var index = context.RegisterInternedString(value);
#if DEBUG
context.OnStringInterned?.Invoke(context.CurrentPropertyPath, value);
#endif
context.WriteByte(BinaryTypeCode.StringInterned);
context.WriteVarUInt((uint)index);
return;
}
// Try FixStr for short ASCII strings (saves 1-2 bytes per string)
if (System.Text.Ascii.IsValid(value) && BinaryTypeCode.CanEncodeAsFixStr(value.Length))
// Fast path for short strings: check length first (cheap), then ASCII
// 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;
}
// Standard string encoding
// Long strings - standard encoding
context.WriteByte(BinaryTypeCode.String);
context.WriteStringUtf8(value);
}
@ -642,29 +819,29 @@ public static partial class AcBinarySerializer
var metadata = wrapper.Metadata;
// Wire format:
// - 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
// - Ref=Off: [Object][props 0-tól...] - semmi 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
// - Ref=Off: [Object][props 0-tól...] - semmi extra
// ObjectRef format:
// - IId: [ObjectRef][Id érték]
// - IId: [ObjectRef][Id érték]
// - Non-IId: [ObjectRef][hashcode]
if (context.UseTypeReferenceHandling(metadata))
{
if (metadata.IsIId)
{
// IId típus: track by Id, ObjectRef writes Id
// IId típus: track by Id, ObjectRef writes Id
switch (metadata.IdAccessorType)
{
case AcSerializerCommon.IdAccessorType.Int32:
if (!context.TryTrack(wrapper, value, out int intId))
{
// Already seen ? ObjectRef + Id
// Already seen ObjectRef + Id
context.WriteByte(BinaryTypeCode.ObjectRef);
context.WriteVarInt(intId);
return;
}
// First occurrence ? Object (no extra data, Id in props)
// First occurrence Object (no extra data, Id in props)
context.WriteByte(BinaryTypeCode.Object);
break;
@ -694,12 +871,12 @@ public static partial class AcBinarySerializer
// Non-IId + RefHandling=All: track by hashcode (IdAccessorType=Int32, RefIdGetter=GetHashCode)
if (!context.TryTrack(wrapper, value, out int hashcode))
{
// Already seen ? ObjectRef + hashcode
// Already seen ObjectRef + hashcode
context.WriteByte(BinaryTypeCode.ObjectRef);
context.WriteVarInt(hashcode);
return;
}
// First occurrence ? Object + hashcode + props
// First occurrence Object + hashcode + props
context.WriteByte(BinaryTypeCode.Object);
context.WriteVarInt(hashcode);
}
@ -846,6 +1023,16 @@ public static partial class AcBinarySerializer
context.WriteVarInt(enumValue);
}
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:
// Fallback to object getter for reference types
var value = prop.GetValue(obj);
@ -1002,6 +1189,23 @@ public static partial class AcBinarySerializer
}
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:
{
// Object type - use regular getter
@ -1015,6 +1219,9 @@ public static partial class AcBinarySerializer
}
else
{
#if DEBUG
context.CurrentPropertyPath = $"{prop.DeclaringType.Name}.{prop.Name}";
#endif
WriteValue(value, prop.PropertyType, context, depth);
}
return;

View File

@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
using AyCode.Core.Serializers.Jsons;
namespace AyCode.Core.Serializers.Binaries;
@ -23,14 +22,16 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// <summary>
/// Default options instance with metadata and string interning enabled.
/// Returns a new instance each time to prevent shared state corruption.
/// </summary>
public static readonly AcBinarySerializerOptions Default = new();
public static AcBinarySerializerOptions Default => new();
/// <summary>
/// Options optimized for maximum speed (no interning, no references).
/// Use when deserializer knows the exact type structure.
/// Returns a new instance each time to prevent shared state corruption.
/// </summary>
public static readonly AcBinarySerializerOptions FastMode = new()
public static AcBinarySerializerOptions FastMode => new()
{
UseStringInterning = false,
ReferenceHandling = ReferenceHandlingMode.None
@ -38,8 +39,9 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// <summary>
/// Options for shallow serialization (root level only).
/// Returns a new instance each time to prevent shared state corruption.
/// </summary>
public static readonly AcBinarySerializerOptions ShallowCopy = new()
public static AcBinarySerializerOptions ShallowCopy => new()
{
MaxDepth = 0,
UseStringInterning = false,
@ -48,8 +50,9 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// <summary>
/// Options optimized for WASM environment with string caching enabled.
/// Returns a new instance each time to prevent shared state corruption.
/// </summary>
public static readonly AcBinarySerializerOptions WasmOptimized = new()
public static AcBinarySerializerOptions WasmOptimized => new()
{
IsWasm = true,
UseStringCaching = true
@ -91,7 +94,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// Reduces size and memory for objects with many repeated string values.
/// Default: true
/// </summary>
public bool UseStringInterning { get; init; } = true;
public bool UseStringInterning { get; set; } = true;
/// <summary>
/// Minimum string length to consider for interning.
@ -137,8 +140,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// <summary>
/// Creates options without reference handling (and string interning disabled for speed).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static AcBinarySerializerOptions WithoutReferenceHandling() => new()
public static AcBinarySerializerOptions WithoutReferenceHandling => new()
{
ReferenceHandling = ReferenceHandlingMode.None,
};
@ -146,213 +148,5 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// <summary>
/// Creates options without metadata (faster but less flexible).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
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);
}
}
public static AcBinarySerializerOptions WithoutMetadata => new() { UseMetadata = false };
}

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 AyCode.Core.Serializers.Jsons;
namespace AyCode.Core.Serializers;

View File

@ -1,92 +1,7 @@
using System.Reflection;
using System.Reflection;
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>
/// Options for AcJsonSerializer and AcJsonDeserializer.
/// </summary>
@ -96,13 +11,15 @@ public sealed class AcJsonSerializerOptions : AcSerializerOptions
/// <summary>
/// Default options instance with reference handling enabled and max depth.
/// Returns a new instance each time to prevent shared state corruption.
/// </summary>
public static readonly AcJsonSerializerOptions Default = new() { ReferenceHandling = ReferenceHandlingMode.All };
public static AcJsonSerializerOptions Default => new() { ReferenceHandling = ReferenceHandlingMode.All };
/// <summary>
/// Options for shallow serialization (root level only, no references).
/// Returns a new instance each time to prevent shared state corruption.
/// </summary>
public static readonly AcJsonSerializerOptions ShallowCopy = new() { MaxDepth = 0, ReferenceHandling = ReferenceHandlingMode.None };
public static AcJsonSerializerOptions ShallowCopy => new() { MaxDepth = 0, ReferenceHandling = ReferenceHandlingMode.None };
/// <summary>
/// Creates options with specified max depth.
@ -112,5 +29,5 @@ public sealed class AcJsonSerializerOptions : AcSerializerOptions
/// <summary>
/// Creates options without reference handling.
/// </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, ulong>? _uint64Getter;
private readonly Func<object, Guid>? _guidGetter;
private readonly Func<object, string?>? _stringGetter;
#endregion
@ -82,6 +83,9 @@ public abstract class PropertyAccessorBase : PropertyMetadataBase
case PropertyAccessorType.Enum:
Unsafe.AsRef(in _int32Getter) = AcSerializerCommon.CreateEnumGetter(declaringType, prop);
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)]
public int GetEnumAsInt32(object obj) => _int32Getter!(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string? GetString(object obj) => _stringGetter!(obj);
#endregion
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ using AyCode.Services.Server.SignalRs;
using AyCode.Services.SignalRs;
using MessagePack.Resolvers;
using AyCode.Core.Tests.Serialization;
using AyCode.Core.Serializers;
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.Services.Server.SignalRs;
using AyCode.Services.SignalRs;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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