diff --git a/AyCode.Benchmark/AyCode.Benchmark.csproj b/AyCode.Benchmark/AyCode.Benchmark.csproj index 346759e..473f685 100644 --- a/AyCode.Benchmark/AyCode.Benchmark.csproj +++ b/AyCode.Benchmark/AyCode.Benchmark.csproj @@ -28,6 +28,10 @@ + + diff --git a/AyCode.Benchmark/SourceGeneratorBenchmarks.cs b/AyCode.Benchmark/SourceGeneratorBenchmarks.cs new file mode 100644 index 0000000..1b30b49 --- /dev/null +++ b/AyCode.Benchmark/SourceGeneratorBenchmarks.cs @@ -0,0 +1,294 @@ +using AyCode.Core.Serializers.Attributes; +using AyCode.Core.Serializers.Binaries; +using BenchmarkDotNet.Attributes; +using MessagePack; +using MessagePack.Resolvers; + +namespace AyCode.Core.Benchmarks; + +/// +/// Pure contractless model - NO MessagePack attributes. +/// This tests TRUE runtime serialization without any source generation. +/// +[AcBinarySerializable] +public class PureContractlessModel +{ + public int Id { get; set; } + public string Name { get; set; } = ""; + public string Description { get; set; } = ""; + public bool IsActive { get; set; } + public double Value { get; set; } + public decimal Price { get; set; } + public long BigNumber { get; set; } + public DateTime CreatedAt { get; set; } + public Guid UniqueId { get; set; } + public int Count { get; set; } + public string Category { get; set; } = ""; + public string Status { get; set; } = ""; +} + +/// +/// Benchmark model with only primitive types - fully supported by Source Generator. +/// MessagePack attributes added for fair comparison with Source Generator. +/// +[AcBinarySerializable] +[MessagePackObject] +public class PrimitiveBenchmarkModel +{ + [Key(0)] public int Id { get; set; } + [Key(1)] public string Name { get; set; } = ""; + [Key(2)] public string Description { get; set; } = ""; + [Key(3)] public bool IsActive { get; set; } + [Key(4)] public double Value { get; set; } + [Key(5)] public decimal Price { get; set; } + [Key(6)] public long BigNumber { get; set; } + [Key(7)] public DateTime CreatedAt { get; set; } + [Key(8)] public Guid UniqueId { get; set; } + [Key(9)] public int Count { get; set; } + [Key(10)] public string Category { get; set; } = ""; + [Key(11)] public string Status { get; set; } = ""; +} + +/// +/// TRUE Contractless benchmark - no attributes, pure runtime serialization. +/// +[ShortRunJob] +[MemoryDiagnoser] +[RankColumn] +public class PureContractlessBenchmark +{ + private PureContractlessModel _testData = null!; + private byte[] _acBinaryData = null!; + private byte[] _msgPackContractlessData = null!; + + private AcBinarySerializerOptions _binaryOptions = null!; + private MessagePackSerializerOptions _msgPackContractlessOptions = null!; + + [GlobalSetup] + public void Setup() + { + _testData = new PureContractlessModel + { + Id = 12345, + Name = "Test Product Name", + Description = "This is a longer description for testing string serialization performance", + IsActive = true, + Value = 123.456789, + Price = 99.99m, + BigNumber = 9876543210L, + CreatedAt = DateTime.UtcNow, + UniqueId = Guid.NewGuid(), + Count = 42, + Category = "Electronics", + Status = "Available" + }; + + _binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); + _msgPackContractlessOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); + + _acBinaryData = AcBinarySerializer.Serialize(_testData, _binaryOptions); + _msgPackContractlessData = MessagePackSerializer.Serialize(_testData, _msgPackContractlessOptions); + + Console.WriteLine($"=== Pure Contractless (NO attributes) ==="); + Console.WriteLine($"AcBinary: {_acBinaryData.Length} bytes"); + Console.WriteLine($"MsgPack Contractless: {_msgPackContractlessData.Length} bytes"); + } + + [Benchmark(Description = "AcBinary Serialize")] + public byte[] Serialize_AcBinary() + => AcBinarySerializer.Serialize(_testData, _binaryOptions); + + [Benchmark(Description = "MsgPack Contractless Serialize", Baseline = true)] + public byte[] Serialize_MsgPack_Contractless() + => MessagePackSerializer.Serialize(_testData, _msgPackContractlessOptions); + + [Benchmark(Description = "AcBinary Deserialize")] + public PureContractlessModel? Deserialize_AcBinary() + => AcBinaryDeserializer.Deserialize(_acBinaryData); + + [Benchmark(Description = "MsgPack Contractless Deserialize")] + public PureContractlessModel? Deserialize_MsgPack_Contractless() + => MessagePackSerializer.Deserialize(_msgPackContractlessData, _msgPackContractlessOptions); +} + +/// +/// Benchmark comparing Source Generator vs Runtime serialization for primitive-only types. +/// Uses MessagePack with [MessagePackObject] attributes for fair Source Generator comparison. +/// +[ShortRunJob] +[MemoryDiagnoser] +[RankColumn] +public class SourceGeneratorVsRuntimeBenchmark +{ + private PrimitiveBenchmarkModel _testData = null!; + private byte[] _runtimeSerializedData = null!; + private byte[] _msgPackData = null!; + private byte[] _msgPackContractlessData = null!; + + private AcBinarySerializerOptions _binaryOptions = null!; + private MessagePackSerializerOptions _msgPackOptions = null!; + private MessagePackSerializerOptions _msgPackContractlessOptions = null!; + + [GlobalSetup] + public void Setup() + { + _testData = new PrimitiveBenchmarkModel + { + Id = 12345, + Name = "Test Product Name", + Description = "This is a longer description for testing string serialization performance", + IsActive = true, + Value = 123.456789, + Price = 99.99m, + BigNumber = 9876543210L, + CreatedAt = DateTime.UtcNow, + UniqueId = Guid.NewGuid(), + Count = 42, + Category = "Electronics", + Status = "Available" + }; + + _binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); + + // MessagePack with Source Generator (uses [MessagePackObject] + [Key] attributes) + _msgPackOptions = MessagePackSerializerOptions.Standard; + + // MessagePack without Source Generator (Contractless - reflection based, like AcBinary runtime) + _msgPackContractlessOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); + + // Pre-serialize for deserialize benchmarks + _runtimeSerializedData = AcBinarySerializer.Serialize(_testData, _binaryOptions); + _msgPackData = MessagePackSerializer.Serialize(_testData, _msgPackOptions); + _msgPackContractlessData = MessagePackSerializer.Serialize(_testData, _msgPackContractlessOptions); + + // Print sizes + Console.WriteLine($"=== Primitive Model Serialization ==="); + Console.WriteLine($"AcBinary Runtime: {_runtimeSerializedData.Length} bytes"); + Console.WriteLine($"MessagePack SourceGen: {_msgPackData.Length} bytes"); + Console.WriteLine($"MessagePack Contractless:{_msgPackContractlessData.Length} bytes"); + + // Verify generated serializer exists + var generatedType = typeof(PrimitiveBenchmarkModel).Assembly.GetType( + $"{typeof(PrimitiveBenchmarkModel).FullName}_AcBinarySerializer"); + Console.WriteLine($"AcBinary Generated serializer found: {generatedType != null}"); + } + + #region Serialize Benchmarks + + [Benchmark(Description = "AcBinary Runtime Serialize")] + public byte[] Serialize_AcBinary_Runtime() + => AcBinarySerializer.Serialize(_testData, _binaryOptions); + + [Benchmark(Description = "MsgPack SourceGen Serialize", Baseline = true)] + public byte[] Serialize_MsgPack_SourceGen() + => MessagePackSerializer.Serialize(_testData, _msgPackOptions); + + [Benchmark(Description = "MsgPack Contractless Serialize")] + public byte[] Serialize_MsgPack_Contractless() + => MessagePackSerializer.Serialize(_testData, _msgPackContractlessOptions); + + #endregion + + #region Deserialize Benchmarks + + [Benchmark(Description = "AcBinary Runtime Deserialize")] + public PrimitiveBenchmarkModel? Deserialize_AcBinary_Runtime() + => AcBinaryDeserializer.Deserialize(_runtimeSerializedData); + + [Benchmark(Description = "MsgPack SourceGen Deserialize")] + public PrimitiveBenchmarkModel? Deserialize_MsgPack_SourceGen() + => MessagePackSerializer.Deserialize(_msgPackData, _msgPackOptions); + + [Benchmark(Description = "MsgPack Contractless Deserialize")] + public PrimitiveBenchmarkModel? Deserialize_MsgPack_Contractless() + => MessagePackSerializer.Deserialize(_msgPackContractlessData, _msgPackContractlessOptions); + + #endregion +} + +/// +/// Repeated string benchmark model - tests string interning performance. +/// MessagePack attributes added for fair comparison. +/// +[AcBinarySerializable] +[MessagePackObject] +public class RepeatedStringBenchmarkModel +{ + [Key(0)] public int Id { get; set; } + [Key(1)] public string Status { get; set; } = ""; + [Key(2)] public string Category { get; set; } = ""; + [Key(3)] public string Priority { get; set; } = ""; + [Key(4)] public string Type { get; set; } = ""; +} + +/// +/// Benchmark for types with repeated string values - where AcBinary string interning helps. +/// Compares against both MessagePack SourceGen and Contractless modes. +/// +[ShortRunJob] +[MemoryDiagnoser] +[RankColumn] +public class RepeatedStringBenchmark +{ + private List _items = null!; + private byte[] _acBinaryData = null!; + private byte[] _msgPackData = null!; + private byte[] _msgPackContractlessData = null!; + + private AcBinarySerializerOptions _binaryOptions = null!; + private MessagePackSerializerOptions _msgPackOptions = null!; + private MessagePackSerializerOptions _msgPackContractlessOptions = null!; + + [Params(100, 500)] + public int ItemCount { get; set; } + + [GlobalSetup] + public void Setup() + { + _items = Enumerable.Range(0, ItemCount).Select(i => new RepeatedStringBenchmarkModel + { + Id = i, + Status = i % 3 == 0 ? "Pending" : i % 3 == 1 ? "Processing" : "Completed", + Category = $"Category_{i % 5}", + Priority = i % 2 == 0 ? "High" : "Low", + Type = i % 4 == 0 ? "TypeA" : i % 4 == 1 ? "TypeB" : i % 4 == 2 ? "TypeC" : "TypeD" + }).ToList(); + + _binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); + _msgPackOptions = MessagePackSerializerOptions.Standard; + _msgPackContractlessOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); + + _acBinaryData = AcBinarySerializer.Serialize(_items, _binaryOptions); + _msgPackData = MessagePackSerializer.Serialize(_items, _msgPackOptions); + _msgPackContractlessData = MessagePackSerializer.Serialize(_items, _msgPackContractlessOptions); + + Console.WriteLine($"=== Repeated Strings ({ItemCount} items) ==="); + Console.WriteLine($"AcBinary: {_acBinaryData.Length} bytes"); + Console.WriteLine($"MsgPack SourceGen: {_msgPackData.Length} bytes"); + Console.WriteLine($"MsgPack Contractless: {_msgPackContractlessData.Length} bytes"); + } + + [Benchmark(Description = "AcBinary Serialize")] + public byte[] Serialize_AcBinary() + => AcBinarySerializer.Serialize(_items, _binaryOptions); + + [Benchmark(Description = "MsgPack SourceGen Serialize", Baseline = true)] + public byte[] Serialize_MsgPack_SourceGen() + => MessagePackSerializer.Serialize(_items, _msgPackOptions); + + [Benchmark(Description = "MsgPack Contractless Serialize")] + public byte[] Serialize_MsgPack_Contractless() + => MessagePackSerializer.Serialize(_items, _msgPackContractlessOptions); + + [Benchmark(Description = "AcBinary Deserialize")] + public List? Deserialize_AcBinary() + => AcBinaryDeserializer.Deserialize>(_acBinaryData); + + [Benchmark(Description = "MsgPack SourceGen Deserialize")] + public List? Deserialize_MsgPack_SourceGen() + => MessagePackSerializer.Deserialize>(_msgPackData, _msgPackOptions); + + [Benchmark(Description = "MsgPack Contractless Deserialize")] + public List? Deserialize_MsgPack_Contractless() + => MessagePackSerializer.Deserialize>(_msgPackContractlessData, _msgPackContractlessOptions); +} diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs new file mode 100644 index 0000000..839dd44 --- /dev/null +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs @@ -0,0 +1,778 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace AyCode.Core.Serializers.SourceGenerator; + +/// +/// Source Generator for AcBinary serialization. +/// Generates optimized serialize/deserialize methods for classes marked with [AcBinarySerializable]. +/// +[Generator] +public class AcBinarySourceGenerator : IIncrementalGenerator +{ + private const string AcBinarySerializableAttributeName = "AyCode.Core.Serializers.Attributes.AcBinarySerializableAttribute"; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // Find all classes with [AcBinarySerializable] attribute + var classDeclarations = context.SyntaxProvider + .ForAttributeWithMetadataName( + AcBinarySerializableAttributeName, + predicate: static (node, _) => node is ClassDeclarationSyntax || node is StructDeclarationSyntax, + transform: static (ctx, _) => GetClassInfo(ctx)) + .Where(static info => info != null); + + // Combine with compilation + var compilationAndClasses = context.CompilationProvider.Combine(classDeclarations.Collect()); + + // Generate source + context.RegisterSourceOutput(compilationAndClasses, + static (spc, source) => Execute(source.Left, source.Right, spc)); + } + + private static SerializableClassInfo GetClassInfo(GeneratorAttributeSyntaxContext context) + { + if (!(context.TargetSymbol is INamedTypeSymbol typeSymbol)) + return null; + + var namespaceName = typeSymbol.ContainingNamespace.IsGlobalNamespace + ? string.Empty + : typeSymbol.ContainingNamespace.ToDisplayString(); + + var className = typeSymbol.Name; + var fullTypeName = typeSymbol.ToDisplayString(); + var isStruct = typeSymbol.IsValueType; + + // Check if this is a nested type + var isNestedType = typeSymbol.ContainingType != null; + + // For nested types, we need the full containing type path for method signatures + // e.g. "OuterClass.InnerClass" instead of just "InnerClass" + var typeNameForSignature = isNestedType + ? GetNestedTypeName(typeSymbol) + : className; + + // Get all public properties with getter and setter + // DUPLICATED LOGIC: Same filtering as TypeMetadataBase.GetSerializableProperties() + var properties = new List(); + foreach (var member in typeSymbol.GetMembers()) + { + if (member is IPropertySymbol p && + p.DeclaredAccessibility == Accessibility.Public && + p.GetMethod != null && + p.SetMethod != null && + !p.IsIndexer && + !p.IsStatic) + { + properties.Add(new PropertyInfo( + p.Name, + p.Type.ToDisplayString(), + GetPropertyTypeKind(p.Type), + p.Type.NullableAnnotation == NullableAnnotation.Annotated || IsNullableValueType(p.Type))); + } + } + + // DUPLICATED LOGIC: Same ordering as TypeMetadataBase.GetSerializableProperties() + properties.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); + + return new SerializableClassInfo(namespaceName, className, fullTypeName, isStruct, isNestedType, typeNameForSignature, properties); + } + + /// + /// Gets the nested type name chain (e.g., "OuterClass.MiddleClass.InnerClass") + /// + private static string GetNestedTypeName(INamedTypeSymbol typeSymbol) + { + var parts = new List(); + var current = typeSymbol; + + while (current != null) + { + parts.Insert(0, current.Name); + current = current.ContainingType; + } + + return string.Join(".", parts); + } + + private static bool IsNullableValueType(ITypeSymbol type) + { + return type is INamedTypeSymbol namedType && + namedType.IsGenericType && + namedType.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T; + } + + private static PropertyTypeKind GetPropertyTypeKind(ITypeSymbol type) + { + // Handle nullable value types + if (type is INamedTypeSymbol namedType && namedType.IsGenericType && + namedType.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T) + { + // Get underlying type + var underlyingType = namedType.TypeArguments[0]; + return GetPropertyTypeKindForUnderlying(underlyingType, isNullable: true); + } + + return GetPropertyTypeKindForUnderlying(type, isNullable: false); + } + + private static PropertyTypeKind GetPropertyTypeKindForUnderlying(ITypeSymbol type, bool isNullable) + { + switch (type.SpecialType) + { + case SpecialType.System_String: return PropertyTypeKind.String; + case SpecialType.System_Int32: return isNullable ? PropertyTypeKind.NullableInt32 : PropertyTypeKind.Int32; + case SpecialType.System_Int64: return isNullable ? PropertyTypeKind.NullableInt64 : PropertyTypeKind.Int64; + case SpecialType.System_Int16: return isNullable ? PropertyTypeKind.NullableInt16 : PropertyTypeKind.Int16; + case SpecialType.System_Byte: return isNullable ? PropertyTypeKind.NullableByte : PropertyTypeKind.Byte; + case SpecialType.System_SByte: return isNullable ? PropertyTypeKind.NullableSByte : PropertyTypeKind.SByte; + case SpecialType.System_UInt16: return isNullable ? PropertyTypeKind.NullableUInt16 : PropertyTypeKind.UInt16; + case SpecialType.System_UInt32: return isNullable ? PropertyTypeKind.NullableUInt32 : PropertyTypeKind.UInt32; + case SpecialType.System_UInt64: return isNullable ? PropertyTypeKind.NullableUInt64 : PropertyTypeKind.UInt64; + case SpecialType.System_Boolean: return isNullable ? PropertyTypeKind.NullableBoolean : PropertyTypeKind.Boolean; + case SpecialType.System_Single: return isNullable ? PropertyTypeKind.NullableSingle : PropertyTypeKind.Single; + case SpecialType.System_Double: return isNullable ? PropertyTypeKind.NullableDouble : PropertyTypeKind.Double; + case SpecialType.System_Decimal: return isNullable ? PropertyTypeKind.NullableDecimal : PropertyTypeKind.Decimal; + case SpecialType.System_DateTime: return isNullable ? PropertyTypeKind.NullableDateTime : PropertyTypeKind.DateTime; + default: return GetNonSpecialTypeKind(type, isNullable); + } + } + + private static PropertyTypeKind GetNonSpecialTypeKind(ITypeSymbol type, bool isNullable) + { + var fullName = type.ToDisplayString(); + + if (fullName == "System.Guid") return isNullable ? PropertyTypeKind.NullableGuid : PropertyTypeKind.Guid; + if (fullName == "System.TimeSpan") return isNullable ? PropertyTypeKind.NullableTimeSpan : PropertyTypeKind.TimeSpan; + if (fullName == "System.DateTimeOffset") return isNullable ? PropertyTypeKind.NullableDateTimeOffset : PropertyTypeKind.DateTimeOffset; + if (fullName == "System.DateOnly") return isNullable ? PropertyTypeKind.NullableDateOnly : PropertyTypeKind.DateOnly; + if (fullName == "System.TimeOnly") return isNullable ? PropertyTypeKind.NullableTimeOnly : PropertyTypeKind.TimeOnly; + if (type.TypeKind == TypeKind.Enum) return isNullable ? PropertyTypeKind.NullableEnum : PropertyTypeKind.Enum; + if (IsCollectionType(type)) return PropertyTypeKind.Collection; + if (type.TypeKind == TypeKind.Class || type.TypeKind == TypeKind.Struct) return PropertyTypeKind.Complex; + + return PropertyTypeKind.Unknown; + } + + private static bool IsCollectionType(ITypeSymbol type) + { + if (type is IArrayTypeSymbol) + return true; + + if (type is INamedTypeSymbol namedType) + { + foreach (var iface in namedType.AllInterfaces) + { + if (iface.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T) + return true; + var ifaceName = iface.ToDisplayString(); + if (ifaceName.StartsWith("System.Collections.Generic.IList<") || + ifaceName.StartsWith("System.Collections.Generic.ICollection<")) + return true; + } + } + + return false; + } + + private static void Execute(Compilation compilation, ImmutableArray classes, SourceProductionContext context) + { + if (classes.IsDefaultOrEmpty) + return; + + foreach (var classInfo in classes) + { + if (classInfo == null) + continue; + + var source = GenerateSerializerClass(classInfo); + context.AddSource($"{classInfo.ClassName}_AcBinarySerializer.g.cs", SourceText.From(source, Encoding.UTF8)); + } + } + + private static string GenerateSerializerClass(SerializableClassInfo classInfo) + { + var sb = new StringBuilder(); + + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); + sb.AppendLine(); + sb.AppendLine("using System;"); + sb.AppendLine("using System.Runtime.CompilerServices;"); + sb.AppendLine("using AyCode.Core.Serializers.Binaries;"); + sb.AppendLine(); + + if (!string.IsNullOrEmpty(classInfo.Namespace)) + { + sb.AppendLine($"namespace {classInfo.Namespace}"); + sb.AppendLine("{"); + } + + var indent = string.IsNullOrEmpty(classInfo.Namespace) ? "" : " "; + + sb.AppendLine($"{indent}/// "); + sb.AppendLine($"{indent}/// Generated binary serializer for {classInfo.ClassName}."); + sb.AppendLine($"{indent}/// "); + sb.AppendLine($"{indent}internal static class {classInfo.ClassName}_AcBinarySerializer"); + sb.AppendLine($"{indent}{{"); + + // Generate property count constant + sb.AppendLine($"{indent} public const int PropertyCount = {classInfo.Properties.Count};"); + sb.AppendLine(); + + // Generate property names array for validation + sb.AppendLine($"{indent} /// "); + sb.AppendLine($"{indent} /// Property names in serialization order (alphabetical)."); + sb.AppendLine($"{indent} /// Used for runtime validation against TypeMetadataBase.GetSerializableProperties()."); + sb.AppendLine($"{indent} /// "); + sb.Append($"{indent} public static readonly string[] PropertyNames = new[] {{ "); + sb.Append(string.Join(", ", classInfo.Properties.Select(p => $"\"{p.Name}\""))); + sb.AppendLine(" };"); + sb.AppendLine(); + + // Generate Serialize method + GenerateSerializeMethod(sb, classInfo, indent); + + // Generate Deserialize method + GenerateDeserializeMethod(sb, classInfo, indent); + + sb.AppendLine($"{indent}}}"); + + if (!string.IsNullOrEmpty(classInfo.Namespace)) + { + sb.AppendLine("}"); + } + + return sb.ToString(); + } + + private static void GenerateSerializeMethod(StringBuilder sb, SerializableClassInfo classInfo, string indent) + { + sb.AppendLine($"{indent} /// "); + sb.AppendLine($"{indent} /// Serializes a {classInfo.ClassName} instance to the binary context."); + sb.AppendLine($"{indent} /// Direct property access - no reflection, no boxing for primitives."); + sb.AppendLine($"{indent} /// "); + sb.AppendLine($"{indent} [MethodImpl(MethodImplOptions.AggressiveInlining)]"); + sb.AppendLine($"{indent} public static void Serialize({classInfo.TypeNameForSignature} obj, ref AcBinarySerializer.BinarySerializationContext context)"); + sb.AppendLine($"{indent} {{"); + + foreach (var prop in classInfo.Properties) + { + GenerateSerializeProperty(sb, prop, indent + " "); + } + + sb.AppendLine($"{indent} }}"); + sb.AppendLine(); + } + + private static void GenerateSerializeProperty(StringBuilder sb, PropertyInfo prop, string indent) + { + var propAccess = $"obj.{prop.Name}"; + + // Handle nullable VALUE types (Nullable) - these use .HasValue and .Value + if (IsNullableValueTypeKind(prop.TypeKind)) + { + sb.AppendLine($"{indent}// {prop.Name}: {prop.TypeName} (nullable value type)"); + sb.AppendLine($"{indent}if ({propAccess}.HasValue)"); + sb.AppendLine($"{indent}{{"); + GenerateSerializeValue(sb, prop.TypeKind, $"{propAccess}.Value", indent + " "); + sb.AppendLine($"{indent}}}"); + sb.AppendLine($"{indent}else"); + sb.AppendLine($"{indent}{{"); + sb.AppendLine($"{indent} context.WriteByte(BinaryTypeCode.Null);"); + sb.AppendLine($"{indent}}}"); + return; + } + + sb.AppendLine($"{indent}// {prop.Name}: {prop.TypeName}"); + + // String needs null check + if (prop.TypeKind == PropertyTypeKind.String) + { + sb.AppendLine($"{indent}if ({propAccess} == null)"); + sb.AppendLine($"{indent}{{"); + sb.AppendLine($"{indent} context.WriteByte(BinaryTypeCode.Null);"); + sb.AppendLine($"{indent}}}"); + sb.AppendLine($"{indent}else if ({propAccess}.Length == 0)"); + sb.AppendLine($"{indent}{{"); + sb.AppendLine($"{indent} context.WriteByte(BinaryTypeCode.StringEmpty);"); + sb.AppendLine($"{indent}}}"); + sb.AppendLine($"{indent}else"); + sb.AppendLine($"{indent}{{"); + sb.AppendLine($"{indent} context.WriteByte(BinaryTypeCode.String);"); + sb.AppendLine($"{indent} context.WriteStringUtf8({propAccess});"); + sb.AppendLine($"{indent}}}"); + return; + } + + // Nullable reference types (Complex/Collection with ? annotation) - use == null + if (prop.IsNullable && (prop.TypeKind == PropertyTypeKind.Complex || prop.TypeKind == PropertyTypeKind.Collection)) + { + sb.AppendLine($"{indent}if ({propAccess} == null)"); + sb.AppendLine($"{indent}{{"); + sb.AppendLine($"{indent} context.WriteByte(BinaryTypeCode.Null);"); + sb.AppendLine($"{indent}}}"); + sb.AppendLine($"{indent}else"); + sb.AppendLine($"{indent}{{"); + GenerateSerializeValue(sb, prop.TypeKind, propAccess, indent + " "); + sb.AppendLine($"{indent}}}"); + return; + } + + GenerateSerializeValue(sb, prop.TypeKind, propAccess, indent); + } + + /// + /// Checks if the type kind represents a nullable VALUE type (Nullable<T>), not a reference type + /// + private static bool IsNullableValueTypeKind(PropertyTypeKind kind) + { + return kind >= PropertyTypeKind.NullableInt32 && kind <= PropertyTypeKind.NullableEnum; + } + + private static void GenerateSerializeValue(StringBuilder sb, PropertyTypeKind typeKind, string valueExpr, string indent) + { + switch (typeKind) + { + case PropertyTypeKind.Int32: + case PropertyTypeKind.NullableInt32: + sb.AppendLine($"{indent}if (BinaryTypeCode.TryEncodeTinyInt({valueExpr}, out var tiny_{valueExpr.Replace(".", "_")}))"); + sb.AppendLine($"{indent}{{"); + sb.AppendLine($"{indent} context.WriteByte(tiny_{valueExpr.Replace(".", "_")});"); + sb.AppendLine($"{indent}}}"); + sb.AppendLine($"{indent}else"); + sb.AppendLine($"{indent}{{"); + sb.AppendLine($"{indent} context.WriteByte(BinaryTypeCode.Int32);"); + sb.AppendLine($"{indent} context.WriteVarInt({valueExpr});"); + sb.AppendLine($"{indent}}}"); + break; + + case PropertyTypeKind.Int64: + case PropertyTypeKind.NullableInt64: + sb.AppendLine($"{indent}context.WriteByte(BinaryTypeCode.Int64);"); + sb.AppendLine($"{indent}context.WriteVarLong({valueExpr});"); + break; + + case PropertyTypeKind.Boolean: + case PropertyTypeKind.NullableBoolean: + sb.AppendLine($"{indent}context.WriteByte({valueExpr} ? BinaryTypeCode.True : BinaryTypeCode.False);"); + break; + + case PropertyTypeKind.Double: + case PropertyTypeKind.NullableDouble: + sb.AppendLine($"{indent}context.WriteTypeCodeAndRaw(BinaryTypeCode.Float64, {valueExpr});"); + break; + + case PropertyTypeKind.Single: + case PropertyTypeKind.NullableSingle: + sb.AppendLine($"{indent}context.WriteTypeCodeAndRaw(BinaryTypeCode.Float32, {valueExpr});"); + break; + + case PropertyTypeKind.Decimal: + case PropertyTypeKind.NullableDecimal: + sb.AppendLine($"{indent}context.WriteByte(BinaryTypeCode.Decimal);"); + sb.AppendLine($"{indent}context.WriteDecimalBits({valueExpr});"); + break; + + case PropertyTypeKind.DateTime: + case PropertyTypeKind.NullableDateTime: + sb.AppendLine($"{indent}context.WriteByte(BinaryTypeCode.DateTime);"); + sb.AppendLine($"{indent}context.WriteDateTimeBits({valueExpr});"); + break; + + case PropertyTypeKind.Guid: + case PropertyTypeKind.NullableGuid: + sb.AppendLine($"{indent}context.WriteByte(BinaryTypeCode.Guid);"); + sb.AppendLine($"{indent}context.WriteGuidBits({valueExpr});"); + break; + + case PropertyTypeKind.Byte: + case PropertyTypeKind.NullableByte: + sb.AppendLine($"{indent}context.WriteTwoBytes(BinaryTypeCode.UInt8, {valueExpr});"); + break; + + case PropertyTypeKind.Int16: + case PropertyTypeKind.NullableInt16: + sb.AppendLine($"{indent}context.WriteTypeCodeAndRaw(BinaryTypeCode.Int16, {valueExpr});"); + break; + + case PropertyTypeKind.UInt16: + case PropertyTypeKind.NullableUInt16: + sb.AppendLine($"{indent}context.WriteTypeCodeAndRaw(BinaryTypeCode.UInt16, {valueExpr});"); + break; + + case PropertyTypeKind.UInt32: + case PropertyTypeKind.NullableUInt32: + sb.AppendLine($"{indent}context.WriteByte(BinaryTypeCode.UInt32);"); + sb.AppendLine($"{indent}context.WriteVarUInt({valueExpr});"); + sb.AppendLine($"{indent}context.WriteByte(BinaryTypeCode.UInt32);"); + sb.AppendLine($"{indent}context.WriteVarUInt({valueExpr});"); + break; + + case PropertyTypeKind.UInt64: + case PropertyTypeKind.NullableUInt64: + sb.AppendLine($"{indent}context.WriteByte(BinaryTypeCode.UInt64);"); + sb.AppendLine($"{indent}context.WriteVarULong({valueExpr});"); + break; + + case PropertyTypeKind.TimeSpan: + case PropertyTypeKind.NullableTimeSpan: + sb.AppendLine($"{indent}context.WriteTypeCodeAndRaw(BinaryTypeCode.TimeSpan, {valueExpr}.Ticks);"); + break; + + case PropertyTypeKind.DateTimeOffset: + case PropertyTypeKind.NullableDateTimeOffset: + sb.AppendLine($"{indent}context.WriteByte(BinaryTypeCode.DateTimeOffset);"); + sb.AppendLine($"{indent}context.WriteDateTimeOffsetBits({valueExpr});"); + break; + + case PropertyTypeKind.Enum: + case PropertyTypeKind.NullableEnum: + sb.AppendLine($"{indent}context.WriteByte(BinaryTypeCode.Enum);"); + sb.AppendLine($"{indent}var enumVal = (int){valueExpr};"); + sb.AppendLine($"{indent}if (BinaryTypeCode.TryEncodeTinyInt(enumVal, out var tinyEnum))"); + sb.AppendLine($"{indent}{{"); + sb.AppendLine($"{indent} context.WriteByte(tinyEnum);"); + sb.AppendLine($"{indent}}}"); + sb.AppendLine($"{indent}else"); + sb.AppendLine($"{indent}{{"); + sb.AppendLine($"{indent} context.WriteByte(BinaryTypeCode.Int32);"); + sb.AppendLine($"{indent} context.WriteVarInt(enumVal);"); + sb.AppendLine($"{indent}}}"); + break; + + case PropertyTypeKind.Collection: + case PropertyTypeKind.Complex: + // TODO: Collections and complex types will be implemented later + sb.AppendLine($"{indent}// TODO: Complex/Collection type - fallback to runtime serializer"); + sb.AppendLine($"{indent}throw new NotImplementedException(\"Complex/Collection types not yet implemented in generated serializer\");"); + break; + + default: + sb.AppendLine($"{indent}// Unknown type - fallback needed"); + sb.AppendLine($"{indent}throw new NotImplementedException($\"Type {typeKind} not implemented in generated serializer\");"); + break; + } + } + + private static void GenerateDeserializeMethod(StringBuilder sb, SerializableClassInfo classInfo, string indent) + { + sb.AppendLine($"{indent} /// "); + sb.AppendLine($"{indent} /// Deserializes properties into a {classInfo.ClassName} instance from the binary context."); + sb.AppendLine($"{indent} /// Direct property access - no reflection, no boxing for primitives."); + sb.AppendLine($"{indent} /// "); + sb.AppendLine($"{indent} [MethodImpl(MethodImplOptions.AggressiveInlining)]"); + sb.AppendLine($"{indent} public static void Deserialize({classInfo.TypeNameForSignature} obj, ref AcBinaryDeserializer.BinaryDeserializationContext context)"); + sb.AppendLine($"{indent} {{"); + + foreach (var prop in classInfo.Properties) + { + GenerateDeserializeProperty(sb, prop, indent + " "); + } + + sb.AppendLine($"{indent} }}"); + } + + private static void GenerateDeserializeProperty(StringBuilder sb, PropertyInfo prop, string indent) + { + sb.AppendLine($"{indent}// {prop.Name}: {prop.TypeName}"); + sb.AppendLine($"{indent}{{"); + sb.AppendLine($"{indent} var peekCode = context.PeekByte();"); + + // Handle Skip marker + sb.AppendLine($"{indent} if (peekCode == BinaryTypeCode.PropertySkip)"); + sb.AppendLine($"{indent} {{"); + sb.AppendLine($"{indent} context.ReadByte(); // consume Skip marker"); + sb.AppendLine($"{indent} // Property keeps default value"); + sb.AppendLine($"{indent} }}"); + + // Handle Null marker + sb.AppendLine($"{indent} else if (peekCode == BinaryTypeCode.Null)"); + sb.AppendLine($"{indent} {{"); + sb.AppendLine($"{indent} context.ReadByte(); // consume Null marker"); + if (prop.IsNullable || prop.TypeKind == PropertyTypeKind.String) + { + sb.AppendLine($"{indent} obj.{prop.Name} = default;"); + } + else + { + sb.AppendLine($"{indent} // Non-nullable property, keep default"); + } + sb.AppendLine($"{indent} }}"); + + // Handle actual value + sb.AppendLine($"{indent} else"); + sb.AppendLine($"{indent} {{"); + GenerateDeserializeValue(sb, prop, indent + " "); + sb.AppendLine($"{indent} }}"); + + sb.AppendLine($"{indent}}}"); + } + + private static void GenerateDeserializeValue(StringBuilder sb, PropertyInfo prop, string indent) + { + var propAccess = $"obj.{prop.Name}"; + + switch (prop.TypeKind) + { + case PropertyTypeKind.String: + sb.AppendLine($"{indent}if (peekCode == BinaryTypeCode.StringEmpty)"); + sb.AppendLine($"{indent}{{"); + sb.AppendLine($"{indent} context.ReadByte();"); + sb.AppendLine($"{indent} {propAccess} = string.Empty;"); + sb.AppendLine($"{indent}}}"); + sb.AppendLine($"{indent}else if (peekCode == BinaryTypeCode.String)"); + sb.AppendLine($"{indent}{{"); + sb.AppendLine($"{indent} context.ReadByte();"); + sb.AppendLine($"{indent} var len = (int)context.ReadVarUInt();"); + sb.AppendLine($"{indent} {propAccess} = context.ReadStringUtf8(len);"); + sb.AppendLine($"{indent}}}"); + sb.AppendLine($"{indent}else if (BinaryTypeCode.IsFixStr(peekCode))"); + sb.AppendLine($"{indent}{{"); + sb.AppendLine($"{indent} context.ReadByte();"); + sb.AppendLine($"{indent} var len = BinaryTypeCode.DecodeFixStrLength(peekCode);"); + sb.AppendLine($"{indent} {propAccess} = len == 0 ? string.Empty : context.ReadStringUtf8(len);"); + sb.AppendLine($"{indent}}}"); + break; + + case PropertyTypeKind.Int32: + case PropertyTypeKind.NullableInt32: + sb.AppendLine($"{indent}if (BinaryTypeCode.IsTinyInt(peekCode))"); + sb.AppendLine($"{indent}{{"); + sb.AppendLine($"{indent} context.ReadByte();"); + sb.AppendLine($"{indent} {propAccess} = BinaryTypeCode.DecodeTinyInt(peekCode);"); + sb.AppendLine($"{indent}}}"); + sb.AppendLine($"{indent}else if (peekCode == BinaryTypeCode.Int32)"); + sb.AppendLine($"{indent}{{"); + sb.AppendLine($"{indent} context.ReadByte();"); + sb.AppendLine($"{indent} {propAccess} = context.ReadVarInt();"); + sb.AppendLine($"{indent}}}"); + break; + + case PropertyTypeKind.Int64: + case PropertyTypeKind.NullableInt64: + sb.AppendLine($"{indent}if (BinaryTypeCode.IsTinyInt(peekCode))"); + sb.AppendLine($"{indent}{{"); + sb.AppendLine($"{indent} context.ReadByte();"); + sb.AppendLine($"{indent} {propAccess} = BinaryTypeCode.DecodeTinyInt(peekCode);"); + sb.AppendLine($"{indent}}}"); + sb.AppendLine($"{indent}else if (peekCode == BinaryTypeCode.Int32)"); + sb.AppendLine($"{indent}{{"); + sb.AppendLine($"{indent} context.ReadByte();"); + sb.AppendLine($"{indent} {propAccess} = context.ReadVarInt();"); + sb.AppendLine($"{indent}}}"); + sb.AppendLine($"{indent}else if (peekCode == BinaryTypeCode.Int64)"); + sb.AppendLine($"{indent}{{"); + sb.AppendLine($"{indent} context.ReadByte();"); + sb.AppendLine($"{indent} {propAccess} = context.ReadVarLong();"); + sb.AppendLine($"{indent}}}"); + break; + + case PropertyTypeKind.Boolean: + case PropertyTypeKind.NullableBoolean: + sb.AppendLine($"{indent}context.ReadByte();"); + sb.AppendLine($"{indent}{propAccess} = peekCode == BinaryTypeCode.True;"); + break; + + case PropertyTypeKind.Double: + case PropertyTypeKind.NullableDouble: + sb.AppendLine($"{indent}context.ReadByte();"); + sb.AppendLine($"{indent}{propAccess} = context.ReadDoubleUnsafe();"); + break; + + case PropertyTypeKind.Single: + case PropertyTypeKind.NullableSingle: + sb.AppendLine($"{indent}context.ReadByte();"); + sb.AppendLine($"{indent}{propAccess} = context.ReadSingleUnsafe();"); + break; + + case PropertyTypeKind.Decimal: + case PropertyTypeKind.NullableDecimal: + sb.AppendLine($"{indent}context.ReadByte();"); + sb.AppendLine($"{indent}{propAccess} = context.ReadDecimalUnsafe();"); + break; + + case PropertyTypeKind.DateTime: + case PropertyTypeKind.NullableDateTime: + sb.AppendLine($"{indent}context.ReadByte();"); + sb.AppendLine($"{indent}{propAccess} = context.ReadDateTimeUnsafe();"); + break; + + case PropertyTypeKind.Guid: + case PropertyTypeKind.NullableGuid: + sb.AppendLine($"{indent}context.ReadByte();"); + sb.AppendLine($"{indent}{propAccess} = context.ReadGuidUnsafe();"); + break; + + case PropertyTypeKind.Byte: + case PropertyTypeKind.NullableByte: + sb.AppendLine($"{indent}if (BinaryTypeCode.IsTinyInt(peekCode))"); + sb.AppendLine($"{indent}{{"); + sb.AppendLine($"{indent} context.ReadByte();"); + sb.AppendLine($"{indent} {propAccess} = (byte)BinaryTypeCode.DecodeTinyInt(peekCode);"); + sb.AppendLine($"{indent}}}"); + sb.AppendLine($"{indent}else"); + sb.AppendLine($"{indent}{{"); + sb.AppendLine($"{indent} context.ReadByte();"); + sb.AppendLine($"{indent} {propAccess} = context.ReadByte();"); + sb.AppendLine($"{indent}}}"); + break; + + case PropertyTypeKind.Int16: + case PropertyTypeKind.NullableInt16: + sb.AppendLine($"{indent}context.ReadByte();"); + sb.AppendLine($"{indent}{propAccess} = context.ReadInt16Unsafe();"); + break; + + case PropertyTypeKind.UInt16: + case PropertyTypeKind.NullableUInt16: + sb.AppendLine($"{indent}context.ReadByte();"); + sb.AppendLine($"{indent}{propAccess} = context.ReadUInt16Unsafe();"); + break; + + case PropertyTypeKind.UInt32: + case PropertyTypeKind.NullableUInt32: + sb.AppendLine($"{indent}context.ReadByte();"); + sb.AppendLine($"{indent}{propAccess} = context.ReadVarUInt();"); + break; + + case PropertyTypeKind.UInt64: + case PropertyTypeKind.NullableUInt64: + sb.AppendLine($"{indent}context.ReadByte();"); + sb.AppendLine($"{indent}{propAccess} = context.ReadVarULong();"); + break; + + case PropertyTypeKind.TimeSpan: + case PropertyTypeKind.NullableTimeSpan: + sb.AppendLine($"{indent}context.ReadByte();"); + sb.AppendLine($"{indent}{propAccess} = context.ReadTimeSpanUnsafe();"); + break; + + case PropertyTypeKind.DateTimeOffset: + case PropertyTypeKind.NullableDateTimeOffset: + sb.AppendLine($"{indent}context.ReadByte();"); + sb.AppendLine($"{indent}{propAccess} = context.ReadDateTimeOffsetUnsafe();"); + break; + + case PropertyTypeKind.Enum: + case PropertyTypeKind.NullableEnum: + sb.AppendLine($"{indent}// TODO: Enum deserialization needs type info"); + sb.AppendLine($"{indent}throw new NotImplementedException(\"Enum deserialization not yet implemented\");"); + break; + + case PropertyTypeKind.Collection: + case PropertyTypeKind.Complex: + sb.AppendLine($"{indent}// TODO: Complex/Collection types"); + sb.AppendLine($"{indent}throw new NotImplementedException(\"Complex/Collection deserialization not yet implemented\");"); + break; + + default: + sb.AppendLine($"{indent}throw new NotImplementedException($\"Type deserialization not implemented\");"); + break; + } + } +} + +/// +/// Information about a class marked with [AcBinarySerializable]. +/// +internal sealed class SerializableClassInfo +{ + public string Namespace { get; } + public string ClassName { get; } + public string FullTypeName { get; } + public bool IsStruct { get; } + public bool IsNestedType { get; } + /// + /// The type name to use in method signatures. For nested types this includes the containing types. + /// e.g., "OuterClass.InnerClass" + /// + public string TypeNameForSignature { get; } + public List Properties { get; } + + public SerializableClassInfo(string ns, string className, string fullTypeName, bool isStruct, bool isNestedType, string typeNameForSignature, List properties) + { + Namespace = ns; + ClassName = className; + FullTypeName = fullTypeName; + IsStruct = isStruct; + IsNestedType = isNestedType; + TypeNameForSignature = typeNameForSignature; + Properties = properties; + } +} + +/// +/// Information about a serializable property. +/// +internal sealed class PropertyInfo +{ + public string Name { get; } + public string TypeName { get; } + public PropertyTypeKind TypeKind { get; } + public bool IsNullable { get; } + + public PropertyInfo(string name, string typeName, PropertyTypeKind typeKind, bool isNullable) + { + Name = name; + TypeName = typeName; + TypeKind = typeKind; + IsNullable = isNullable; + } +} + +/// +/// Kind of property type for code generation. +/// +internal enum PropertyTypeKind +{ + Unknown, + String, + Int32, + Int64, + Int16, + Byte, + SByte, + UInt16, + UInt32, + UInt64, + Boolean, + Single, + Double, + Decimal, + DateTime, + DateTimeOffset, + TimeSpan, + DateOnly, + TimeOnly, + Guid, + Enum, + Collection, + Complex, + // Nullable variants + NullableInt32, + NullableInt64, + NullableInt16, + NullableByte, + NullableSByte, + NullableUInt16, + NullableUInt32, + NullableUInt64, + NullableBoolean, + NullableSingle, + NullableDouble, + NullableDecimal, + NullableDateTime, + NullableDateTimeOffset, + NullableTimeSpan, + NullableDateOnly, + NullableTimeOnly, + NullableGuid, + NullableEnum +} diff --git a/AyCode.Core.Serializers.SourceGenerator/AyCode.Core.Serializers.SourceGenerator.csproj b/AyCode.Core.Serializers.SourceGenerator/AyCode.Core.Serializers.SourceGenerator.csproj new file mode 100644 index 0000000..453d398 Binary files /dev/null and b/AyCode.Core.Serializers.SourceGenerator/AyCode.Core.Serializers.SourceGenerator.csproj differ diff --git a/AyCode.Core.Tests/AyCode.Core.Tests.csproj b/AyCode.Core.Tests/AyCode.Core.Tests.csproj index 9e9f7d9..1bf6239 100644 --- a/AyCode.Core.Tests/AyCode.Core.Tests.csproj +++ b/AyCode.Core.Tests/AyCode.Core.Tests.csproj @@ -3,6 +3,9 @@ false true + + true + $(BaseIntermediateOutputPath)Generated @@ -25,6 +28,9 @@ + diff --git a/AyCode.Core.Tests/Serialization/GeneratedSerializerIntegrationTests.cs b/AyCode.Core.Tests/Serialization/GeneratedSerializerIntegrationTests.cs new file mode 100644 index 0000000..ee4a387 --- /dev/null +++ b/AyCode.Core.Tests/Serialization/GeneratedSerializerIntegrationTests.cs @@ -0,0 +1,149 @@ +using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Tests.TestModels; + +namespace AyCode.Core.Tests.Serialization; + +/// +/// Tests for Source Generator based serialization integration. +/// +[TestClass] +public class GeneratedSerializerIntegrationTests +{ + [TestMethod] + public void GeneratedSerializerType_Exists_ForMarkedTypes() + { + // Arrange - types marked with [AcBinarySerializable] + var type = typeof(GeneratedSerializerTestModel); + + // Act - find the generated serializer type directly + var generatedTypeName = $"{type.FullName}_AcBinarySerializer"; + var serializerType = type.Assembly.GetType(generatedTypeName); + + // Assert + Assert.IsNotNull(serializerType, + $"Generated serializer type '{generatedTypeName}' should exist for [AcBinarySerializable] marked type"); + } + + [TestMethod] + public void GeneratedSerializerType_HasCorrectMethods() + { + // Arrange + var type = typeof(SimpleGeneratedModel); + + // Act - find the generated serializer type directly + var generatedTypeName = $"{type.FullName}_AcBinarySerializer"; + var serializerType = type.Assembly.GetType(generatedTypeName); + + // Assert + Assert.IsNotNull(serializerType, $"Generated serializer type '{generatedTypeName}' should exist"); + + var serializeMethod = serializerType.GetMethod("Serialize", + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); + Assert.IsNotNull(serializeMethod, "Serialize method should exist"); + + var deserializeMethod = serializerType.GetMethod("Deserialize", + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); + Assert.IsNotNull(deserializeMethod, "Deserialize method should exist"); + + var propertyNamesField = serializerType.GetField("PropertyNames", + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); + Assert.IsNotNull(propertyNamesField, "PropertyNames field should exist"); + + var propertyNames = propertyNamesField.GetValue(null) as string[]; + Assert.IsNotNull(propertyNames, "PropertyNames should not be null"); + Assert.AreEqual(3, propertyNames.Length, "SimpleGeneratedModel has 3 properties"); + + // Verify alphabetical order + Assert.AreEqual("Age", propertyNames[0]); + Assert.AreEqual("FirstName", propertyNames[1]); + Assert.AreEqual("LastName", propertyNames[2]); + } + + [TestMethod] + public void GeneratedSerializerPropertyNames_MatchRuntimeOrder() + { + // This test verifies that the generated property order matches the runtime serializer's order + // This is critical for binary compatibility! + + var type = typeof(GeneratedSerializerTestModel); + + // Get generated property names + var generatedTypeName = $"{type.FullName}_AcBinarySerializer"; + var serializerType = type.Assembly.GetType(generatedTypeName); + Assert.IsNotNull(serializerType); + + var propertyNamesField = serializerType.GetField("PropertyNames", + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); + var generatedNames = propertyNamesField?.GetValue(null) as string[]; + Assert.IsNotNull(generatedNames); + + // Get runtime property names using the same logic as TypeMetadataBase + var runtimeProps = type.GetProperties( + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance) + .Where(p => p.CanRead && p.CanWrite && !p.GetIndexParameters().Any()) + .OrderBy(p => p.Name, StringComparer.Ordinal) + .Select(p => p.Name) + .ToArray(); + + // Assert they match + CollectionAssert.AreEqual(runtimeProps, generatedNames, + "Generated property names must match runtime property order for binary compatibility"); + } + + [TestMethod] + public void Serialization_WorksCorrectly_WithGeneratedSerializerPresent() + { + // This test ensures that regular serialization still works even when + // generated serializers are present (they are not yet integrated into the hot path) + + var original = new GeneratedSerializerTestModel + { + Id = 42, + Name = "Test", + IsActive = true, + Value = 3.14, + Created = new DateTime(2025, 1, 5, 10, 30, 0, DateTimeKind.Utc), + UniqueId = Guid.NewGuid(), + Price = 99.99m, + BigNumber = 9999999999L + }; + + // Serialize and deserialize using the regular path + var bytes = AcBinarySerializer.Serialize(original, AcBinarySerializerOptions.WithoutReferenceHandling()); + var deserialized = AcBinaryDeserializer.Deserialize(bytes); + + // Assert + Assert.IsNotNull(deserialized); + Assert.AreEqual(original.Id, deserialized.Id); + Assert.AreEqual(original.Name, deserialized.Name); + Assert.AreEqual(original.IsActive, deserialized.IsActive); + Assert.AreEqual(original.Value, deserialized.Value); + Assert.AreEqual(original.Created, deserialized.Created); + Assert.AreEqual(original.UniqueId, deserialized.UniqueId); + Assert.AreEqual(original.Price, deserialized.Price); + Assert.AreEqual(original.BigNumber, deserialized.BigNumber); + } + + [TestMethod] + public void NestedType_GeneratedSerializer_IsFound() + { + // Test that nested types (like QuickBenchmark.TestClassWithRepeatedValues) + // have their generated serializers properly named and discoverable + + var type = typeof(QuickBenchmark.TestClassWithRepeatedValues); + var ns = type.Namespace ?? ""; + + // For nested types, the generated class is at namespace level with just the type name + var simpleName = $"{(string.IsNullOrEmpty(ns) ? "" : ns + ".")}{type.Name}_AcBinarySerializer"; + var serializerType = type.Assembly.GetType(simpleName); + + Assert.IsNotNull(serializerType, + $"Generated serializer for nested type should be found at '{simpleName}'"); + + // Verify it has the expected methods + Assert.IsNotNull(serializerType.GetMethod("Serialize", + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)); + Assert.IsNotNull(serializerType.GetMethod("Deserialize", + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)); + } +} diff --git a/AyCode.Core.Tests/Serialization/QuickBenchmark.cs b/AyCode.Core.Tests/Serialization/QuickBenchmark.cs index 9aaf3fc..27031a5 100644 --- a/AyCode.Core.Tests/Serialization/QuickBenchmark.cs +++ b/AyCode.Core.Tests/Serialization/QuickBenchmark.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using AyCode.Core.Extensions; +using AyCode.Core.Serializers.Attributes; using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Jsons; using AyCode.Core.Tests.TestModels; @@ -666,6 +667,7 @@ public class QuickBenchmark #region Test Models + [AcBinarySerializable] public class TestClassWithRepeatedValues { public int Id { get; set; } diff --git a/AyCode.Core.Tests/TestModels/GeneratedSerializerTestModels.cs b/AyCode.Core.Tests/TestModels/GeneratedSerializerTestModels.cs new file mode 100644 index 0000000..3b6b5ec --- /dev/null +++ b/AyCode.Core.Tests/TestModels/GeneratedSerializerTestModels.cs @@ -0,0 +1,31 @@ +using AyCode.Core.Serializers.Attributes; + +namespace AyCode.Core.Tests.TestModels; + +/// +/// Test model for Source Generator based binary serialization. +/// This class will have generated Serialize/Deserialize methods. +/// +[AcBinarySerializable] +public class GeneratedSerializerTestModel +{ + public int Id { get; set; } + public string? Name { get; set; } + public bool IsActive { get; set; } + public double Value { get; set; } + public DateTime Created { get; set; } + public Guid UniqueId { get; set; } + public decimal Price { get; set; } + public long BigNumber { get; set; } +} + +/// +/// Simple test model with only primitives. +/// +[AcBinarySerializable] +public class SimpleGeneratedModel +{ + public int Age { get; set; } + public string? FirstName { get; set; } + public string? LastName { get; set; } +} diff --git a/AyCode.Core.Tests/TestModels/SharedTestModels.cs b/AyCode.Core.Tests/TestModels/SharedTestModels.cs index 60949a1..e2c7077 100644 --- a/AyCode.Core.Tests/TestModels/SharedTestModels.cs +++ b/AyCode.Core.Tests/TestModels/SharedTestModels.cs @@ -1,5 +1,6 @@ using AyCode.Core.Extensions; using AyCode.Core.Interfaces; +using AyCode.Core.Serializers.Attributes; using AyCode.Core.Serializers.Jsons; using MessagePack; using MongoDB.Bson.Serialization.Attributes; @@ -51,6 +52,7 @@ public enum TestUserRole /// Shared tag/label - used across multiple entities for cross-reference testing. /// Implements IId<int> for semantic $id/$ref serialization. /// +[AcBinarySerializable] public class SharedTag : IId { public int Id { get; set; } @@ -65,6 +67,7 @@ public class SharedTag : IId /// /// Shared category - for hierarchical cross-reference testing. /// +[AcBinarySerializable] public class SharedCategory : IId { public int Id { get; set; } @@ -80,6 +83,7 @@ public class SharedCategory : IId /// /// Shared user reference - appears in many places to test $ref deduplication. /// +[AcBinarySerializable] public class SharedUser : IId { public int Id { get; set; } @@ -97,6 +101,7 @@ public class SharedUser : IId /// /// User preferences - non-IId nested object /// +[AcBinarySerializable] public class UserPreferences { public string Theme { get; set; } = "light"; @@ -113,6 +118,7 @@ public class UserPreferences /// Non-IId metadata class - uses Newtonsoft PreserveReferencesHandling (numeric $id/$ref). /// Does NOT implement IId, so uses standard Newtonsoft reference tracking. /// +[AcBinarySerializable] public class MetadataInfo { public string Key { get; set; } = ""; @@ -132,6 +138,7 @@ public class MetadataInfo /// /// Level 1: Main order - root of the hierarchy /// +[AcBinarySerializable] public class TestOrder : IId { public int Id { get; set; } @@ -172,6 +179,7 @@ public class TestOrder : IId /// /// Level 2: Order item with pallets /// +[AcBinarySerializable] public class TestOrderItem : IId { public int Id { get; set; } @@ -198,6 +206,7 @@ public class TestOrderItem : IId /// /// Level 3: Pallet containing measurements /// +[AcBinarySerializable] public class TestPallet : IId { public int Id { get; set; } @@ -222,6 +231,7 @@ public class TestPallet : IId /// /// Level 4: Measurement with multiple points /// +[AcBinarySerializable] public class TestMeasurement : IId { public int Id { get; set; } @@ -242,6 +252,7 @@ public class TestMeasurement : IId /// /// Level 5: Deepest level - measurement point /// +[AcBinarySerializable] public class TestMeasurementPoint : IId { public int Id { get; set; } @@ -263,6 +274,7 @@ public class TestMeasurementPoint : IId /// /// Order with Guid Id - for testing Guid-based IId /// +[AcBinarySerializable] public class TestGuidOrder : IId { public Guid Id { get; set; } @@ -274,6 +286,7 @@ public class TestGuidOrder : IId /// /// Item with Guid Id /// +[AcBinarySerializable] public class TestGuidItem : IId { public Guid Id { get; set; } @@ -289,6 +302,7 @@ public class TestGuidItem : IId /// Simulates NopCommerce GenericAttribute - stores key-value pairs where DateTime values /// are stored as strings in the database. /// +[AcBinarySerializable] public class TestGenericAttribute { public int Id { get; set; } @@ -300,6 +314,7 @@ public class TestGenericAttribute /// DTO with GenericAttributes collection - simulates OrderDto with string-stored DateTime values. /// This reproduces the production bug where Binary serialization was thought to corrupt DateTime strings. /// +[AcBinarySerializable] public class TestDtoWithGenericAttributes : IId { public int Id { get; set; } @@ -310,6 +325,7 @@ public class TestDtoWithGenericAttributes : IId /// /// Order with nullable collections for null vs empty testing /// +[AcBinarySerializable] public class TestOrderWithNullableCollections { public int Id { get; set; } @@ -321,6 +337,7 @@ public class TestOrderWithNullableCollections /// /// Class with all primitive types for WASM/serialization testing /// +[AcBinarySerializable] public class PrimitiveTestClass { public int IntValue { get; set; } @@ -343,6 +360,7 @@ public class PrimitiveTestClass /// Class with extended primitive types for full serializer coverage. /// Includes DateTimeOffset, TimeSpan, Dictionary, null properties. /// +[AcBinarySerializable] public class ExtendedPrimitiveTestClass { public int Id { get; set; } @@ -372,6 +390,7 @@ public class ExtendedPrimitiveTestClass /// /// Class with array of objects containing null items for WriteNull coverage /// +[AcBinarySerializable] public class ObjectWithNullItems { public int Id { get; set; } @@ -386,6 +405,7 @@ public class ObjectWithNullItems /// "Server-side" DTO with extra properties that the "client" doesn't know about. /// Used to test SkipValue functionality when deserializing unknown properties. /// +[AcBinarySerializable] public class ServerCustomerDto : IId { public int Id { get; set; } @@ -418,6 +438,7 @@ public class ServerCustomerDto : IId /// the deserializer must skip unknown properties correctly /// while still maintaining string intern table consistency. /// +[AcBinarySerializable] public class ClientCustomerDto : IId { public int Id { get; set; } @@ -431,6 +452,7 @@ public class ClientCustomerDto : IId /// Server DTO with nested objects that client doesn't know about. /// Tests skipping complex nested structures. /// +[AcBinarySerializable] public class ServerOrderWithExtras : IId { public int Id { get; set; } @@ -451,6 +473,7 @@ public class ServerOrderWithExtras : IId /// /// Client version of the order - doesn't have Customer/RelatedCustomers properties. /// +[AcBinarySerializable] public class ClientOrderSimple : IId { public int Id { get; set; } diff --git a/AyCode.Core.sln b/AyCode.Core.sln index 3caa24d..7e33393 100644 --- a/AyCode.Core.sln +++ b/AyCode.Core.sln @@ -50,122 +50,388 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AyCode.Benchmark", "AyCode. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AyCode.Services.Tests", "AyCode.Services.Tests\AyCode.Services.Tests.csproj", "{B8443014-1247-FB9C-7BF4-2CC944075A8B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AyCode.Core.Serializers.SourceGenerator", "AyCode.Core.Serializers.SourceGenerator\AyCode.Core.Serializers.SourceGenerator.csproj", "{4A817897-80A8-4F42-86C5-20447401E0AA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Product|Any CPU = Product|Any CPU + Product|x64 = Product|x64 + Product|x86 = Product|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {8CCC4969-7306-4747-8A58-80AC5A062EE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8CCC4969-7306-4747-8A58-80AC5A062EE1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8CCC4969-7306-4747-8A58-80AC5A062EE1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8CCC4969-7306-4747-8A58-80AC5A062EE1}.Debug|x64.Build.0 = Debug|Any CPU + {8CCC4969-7306-4747-8A58-80AC5A062EE1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8CCC4969-7306-4747-8A58-80AC5A062EE1}.Debug|x86.Build.0 = Debug|Any CPU {8CCC4969-7306-4747-8A58-80AC5A062EE1}.Product|Any CPU.ActiveCfg = Product|Any CPU {8CCC4969-7306-4747-8A58-80AC5A062EE1}.Product|Any CPU.Build.0 = Product|Any CPU + {8CCC4969-7306-4747-8A58-80AC5A062EE1}.Product|x64.ActiveCfg = Product|Any CPU + {8CCC4969-7306-4747-8A58-80AC5A062EE1}.Product|x64.Build.0 = Product|Any CPU + {8CCC4969-7306-4747-8A58-80AC5A062EE1}.Product|x86.ActiveCfg = Product|Any CPU + {8CCC4969-7306-4747-8A58-80AC5A062EE1}.Product|x86.Build.0 = Product|Any CPU {8CCC4969-7306-4747-8A58-80AC5A062EE1}.Release|Any CPU.ActiveCfg = Release|Any CPU {8CCC4969-7306-4747-8A58-80AC5A062EE1}.Release|Any CPU.Build.0 = Release|Any CPU + {8CCC4969-7306-4747-8A58-80AC5A062EE1}.Release|x64.ActiveCfg = Release|Any CPU + {8CCC4969-7306-4747-8A58-80AC5A062EE1}.Release|x64.Build.0 = Release|Any CPU + {8CCC4969-7306-4747-8A58-80AC5A062EE1}.Release|x86.ActiveCfg = Release|Any CPU + {8CCC4969-7306-4747-8A58-80AC5A062EE1}.Release|x86.Build.0 = Release|Any CPU {A2105535-1397-4307-B61B-E49C983353B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A2105535-1397-4307-B61B-E49C983353B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2105535-1397-4307-B61B-E49C983353B9}.Debug|x64.ActiveCfg = Debug|Any CPU + {A2105535-1397-4307-B61B-E49C983353B9}.Debug|x64.Build.0 = Debug|Any CPU + {A2105535-1397-4307-B61B-E49C983353B9}.Debug|x86.ActiveCfg = Debug|Any CPU + {A2105535-1397-4307-B61B-E49C983353B9}.Debug|x86.Build.0 = Debug|Any CPU {A2105535-1397-4307-B61B-E49C983353B9}.Product|Any CPU.ActiveCfg = Product|Any CPU {A2105535-1397-4307-B61B-E49C983353B9}.Product|Any CPU.Build.0 = Product|Any CPU + {A2105535-1397-4307-B61B-E49C983353B9}.Product|x64.ActiveCfg = Product|Any CPU + {A2105535-1397-4307-B61B-E49C983353B9}.Product|x64.Build.0 = Product|Any CPU + {A2105535-1397-4307-B61B-E49C983353B9}.Product|x86.ActiveCfg = Product|Any CPU + {A2105535-1397-4307-B61B-E49C983353B9}.Product|x86.Build.0 = Product|Any CPU {A2105535-1397-4307-B61B-E49C983353B9}.Release|Any CPU.ActiveCfg = Release|Any CPU {A2105535-1397-4307-B61B-E49C983353B9}.Release|Any CPU.Build.0 = Release|Any CPU + {A2105535-1397-4307-B61B-E49C983353B9}.Release|x64.ActiveCfg = Release|Any CPU + {A2105535-1397-4307-B61B-E49C983353B9}.Release|x64.Build.0 = Release|Any CPU + {A2105535-1397-4307-B61B-E49C983353B9}.Release|x86.ActiveCfg = Release|Any CPU + {A2105535-1397-4307-B61B-E49C983353B9}.Release|x86.Build.0 = Release|Any CPU {FB027D80-8949-403B-9A86-8E99F305016E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FB027D80-8949-403B-9A86-8E99F305016E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB027D80-8949-403B-9A86-8E99F305016E}.Debug|x64.ActiveCfg = Debug|Any CPU + {FB027D80-8949-403B-9A86-8E99F305016E}.Debug|x64.Build.0 = Debug|Any CPU + {FB027D80-8949-403B-9A86-8E99F305016E}.Debug|x86.ActiveCfg = Debug|Any CPU + {FB027D80-8949-403B-9A86-8E99F305016E}.Debug|x86.Build.0 = Debug|Any CPU {FB027D80-8949-403B-9A86-8E99F305016E}.Product|Any CPU.ActiveCfg = Product|Any CPU {FB027D80-8949-403B-9A86-8E99F305016E}.Product|Any CPU.Build.0 = Product|Any CPU + {FB027D80-8949-403B-9A86-8E99F305016E}.Product|x64.ActiveCfg = Product|Any CPU + {FB027D80-8949-403B-9A86-8E99F305016E}.Product|x64.Build.0 = Product|Any CPU + {FB027D80-8949-403B-9A86-8E99F305016E}.Product|x86.ActiveCfg = Product|Any CPU + {FB027D80-8949-403B-9A86-8E99F305016E}.Product|x86.Build.0 = Product|Any CPU {FB027D80-8949-403B-9A86-8E99F305016E}.Release|Any CPU.ActiveCfg = Release|Any CPU {FB027D80-8949-403B-9A86-8E99F305016E}.Release|Any CPU.Build.0 = Release|Any CPU + {FB027D80-8949-403B-9A86-8E99F305016E}.Release|x64.ActiveCfg = Release|Any CPU + {FB027D80-8949-403B-9A86-8E99F305016E}.Release|x64.Build.0 = Release|Any CPU + {FB027D80-8949-403B-9A86-8E99F305016E}.Release|x86.ActiveCfg = Release|Any CPU + {FB027D80-8949-403B-9A86-8E99F305016E}.Release|x86.Build.0 = Release|Any CPU {CAB60420-9F66-42D9-B67E-8E837DBA1F30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CAB60420-9F66-42D9-B67E-8E837DBA1F30}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CAB60420-9F66-42D9-B67E-8E837DBA1F30}.Debug|x64.ActiveCfg = Debug|Any CPU + {CAB60420-9F66-42D9-B67E-8E837DBA1F30}.Debug|x64.Build.0 = Debug|Any CPU + {CAB60420-9F66-42D9-B67E-8E837DBA1F30}.Debug|x86.ActiveCfg = Debug|Any CPU + {CAB60420-9F66-42D9-B67E-8E837DBA1F30}.Debug|x86.Build.0 = Debug|Any CPU {CAB60420-9F66-42D9-B67E-8E837DBA1F30}.Product|Any CPU.ActiveCfg = Product|Any CPU {CAB60420-9F66-42D9-B67E-8E837DBA1F30}.Product|Any CPU.Build.0 = Product|Any CPU + {CAB60420-9F66-42D9-B67E-8E837DBA1F30}.Product|x64.ActiveCfg = Product|Any CPU + {CAB60420-9F66-42D9-B67E-8E837DBA1F30}.Product|x64.Build.0 = Product|Any CPU + {CAB60420-9F66-42D9-B67E-8E837DBA1F30}.Product|x86.ActiveCfg = Product|Any CPU + {CAB60420-9F66-42D9-B67E-8E837DBA1F30}.Product|x86.Build.0 = Product|Any CPU {CAB60420-9F66-42D9-B67E-8E837DBA1F30}.Release|Any CPU.ActiveCfg = Release|Any CPU {CAB60420-9F66-42D9-B67E-8E837DBA1F30}.Release|Any CPU.Build.0 = Release|Any CPU + {CAB60420-9F66-42D9-B67E-8E837DBA1F30}.Release|x64.ActiveCfg = Release|Any CPU + {CAB60420-9F66-42D9-B67E-8E837DBA1F30}.Release|x64.Build.0 = Release|Any CPU + {CAB60420-9F66-42D9-B67E-8E837DBA1F30}.Release|x86.ActiveCfg = Release|Any CPU + {CAB60420-9F66-42D9-B67E-8E837DBA1F30}.Release|x86.Build.0 = Release|Any CPU {DC42F79D-EEF0-4F32-8608-230F24C6F22A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DC42F79D-EEF0-4F32-8608-230F24C6F22A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DC42F79D-EEF0-4F32-8608-230F24C6F22A}.Debug|x64.ActiveCfg = Debug|Any CPU + {DC42F79D-EEF0-4F32-8608-230F24C6F22A}.Debug|x64.Build.0 = Debug|Any CPU + {DC42F79D-EEF0-4F32-8608-230F24C6F22A}.Debug|x86.ActiveCfg = Debug|Any CPU + {DC42F79D-EEF0-4F32-8608-230F24C6F22A}.Debug|x86.Build.0 = Debug|Any CPU {DC42F79D-EEF0-4F32-8608-230F24C6F22A}.Product|Any CPU.ActiveCfg = Product|Any CPU {DC42F79D-EEF0-4F32-8608-230F24C6F22A}.Product|Any CPU.Build.0 = Product|Any CPU + {DC42F79D-EEF0-4F32-8608-230F24C6F22A}.Product|x64.ActiveCfg = Product|Any CPU + {DC42F79D-EEF0-4F32-8608-230F24C6F22A}.Product|x64.Build.0 = Product|Any CPU + {DC42F79D-EEF0-4F32-8608-230F24C6F22A}.Product|x86.ActiveCfg = Product|Any CPU + {DC42F79D-EEF0-4F32-8608-230F24C6F22A}.Product|x86.Build.0 = Product|Any CPU {DC42F79D-EEF0-4F32-8608-230F24C6F22A}.Release|Any CPU.ActiveCfg = Release|Any CPU {DC42F79D-EEF0-4F32-8608-230F24C6F22A}.Release|Any CPU.Build.0 = Release|Any CPU + {DC42F79D-EEF0-4F32-8608-230F24C6F22A}.Release|x64.ActiveCfg = Release|Any CPU + {DC42F79D-EEF0-4F32-8608-230F24C6F22A}.Release|x64.Build.0 = Release|Any CPU + {DC42F79D-EEF0-4F32-8608-230F24C6F22A}.Release|x86.ActiveCfg = Release|Any CPU + {DC42F79D-EEF0-4F32-8608-230F24C6F22A}.Release|x86.Build.0 = Release|Any CPU {0B5AC35E-3E71-42DC-B503-80D6D3089F91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0B5AC35E-3E71-42DC-B503-80D6D3089F91}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B5AC35E-3E71-42DC-B503-80D6D3089F91}.Debug|x64.ActiveCfg = Debug|Any CPU + {0B5AC35E-3E71-42DC-B503-80D6D3089F91}.Debug|x64.Build.0 = Debug|Any CPU + {0B5AC35E-3E71-42DC-B503-80D6D3089F91}.Debug|x86.ActiveCfg = Debug|Any CPU + {0B5AC35E-3E71-42DC-B503-80D6D3089F91}.Debug|x86.Build.0 = Debug|Any CPU {0B5AC35E-3E71-42DC-B503-80D6D3089F91}.Product|Any CPU.ActiveCfg = Product|Any CPU {0B5AC35E-3E71-42DC-B503-80D6D3089F91}.Product|Any CPU.Build.0 = Product|Any CPU + {0B5AC35E-3E71-42DC-B503-80D6D3089F91}.Product|x64.ActiveCfg = Product|Any CPU + {0B5AC35E-3E71-42DC-B503-80D6D3089F91}.Product|x64.Build.0 = Product|Any CPU + {0B5AC35E-3E71-42DC-B503-80D6D3089F91}.Product|x86.ActiveCfg = Product|Any CPU + {0B5AC35E-3E71-42DC-B503-80D6D3089F91}.Product|x86.Build.0 = Product|Any CPU {0B5AC35E-3E71-42DC-B503-80D6D3089F91}.Release|Any CPU.ActiveCfg = Release|Any CPU {0B5AC35E-3E71-42DC-B503-80D6D3089F91}.Release|Any CPU.Build.0 = Release|Any CPU + {0B5AC35E-3E71-42DC-B503-80D6D3089F91}.Release|x64.ActiveCfg = Release|Any CPU + {0B5AC35E-3E71-42DC-B503-80D6D3089F91}.Release|x64.Build.0 = Release|Any CPU + {0B5AC35E-3E71-42DC-B503-80D6D3089F91}.Release|x86.ActiveCfg = Release|Any CPU + {0B5AC35E-3E71-42DC-B503-80D6D3089F91}.Release|x86.Build.0 = Release|Any CPU {F8ECCA33-B5EA-490D-B1A1-D33B5E4238A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F8ECCA33-B5EA-490D-B1A1-D33B5E4238A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8ECCA33-B5EA-490D-B1A1-D33B5E4238A5}.Debug|x64.ActiveCfg = Debug|Any CPU + {F8ECCA33-B5EA-490D-B1A1-D33B5E4238A5}.Debug|x64.Build.0 = Debug|Any CPU + {F8ECCA33-B5EA-490D-B1A1-D33B5E4238A5}.Debug|x86.ActiveCfg = Debug|Any CPU + {F8ECCA33-B5EA-490D-B1A1-D33B5E4238A5}.Debug|x86.Build.0 = Debug|Any CPU {F8ECCA33-B5EA-490D-B1A1-D33B5E4238A5}.Product|Any CPU.ActiveCfg = Product|Any CPU {F8ECCA33-B5EA-490D-B1A1-D33B5E4238A5}.Product|Any CPU.Build.0 = Product|Any CPU + {F8ECCA33-B5EA-490D-B1A1-D33B5E4238A5}.Product|x64.ActiveCfg = Product|Any CPU + {F8ECCA33-B5EA-490D-B1A1-D33B5E4238A5}.Product|x64.Build.0 = Product|Any CPU + {F8ECCA33-B5EA-490D-B1A1-D33B5E4238A5}.Product|x86.ActiveCfg = Product|Any CPU + {F8ECCA33-B5EA-490D-B1A1-D33B5E4238A5}.Product|x86.Build.0 = Product|Any CPU {F8ECCA33-B5EA-490D-B1A1-D33B5E4238A5}.Release|Any CPU.ActiveCfg = Release|Any CPU {F8ECCA33-B5EA-490D-B1A1-D33B5E4238A5}.Release|Any CPU.Build.0 = Release|Any CPU + {F8ECCA33-B5EA-490D-B1A1-D33B5E4238A5}.Release|x64.ActiveCfg = Release|Any CPU + {F8ECCA33-B5EA-490D-B1A1-D33B5E4238A5}.Release|x64.Build.0 = Release|Any CPU + {F8ECCA33-B5EA-490D-B1A1-D33B5E4238A5}.Release|x86.ActiveCfg = Release|Any CPU + {F8ECCA33-B5EA-490D-B1A1-D33B5E4238A5}.Release|x86.Build.0 = Release|Any CPU {35D47907-CE4F-435B-823E-A02BE59C16D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {35D47907-CE4F-435B-823E-A02BE59C16D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35D47907-CE4F-435B-823E-A02BE59C16D7}.Debug|x64.ActiveCfg = Debug|Any CPU + {35D47907-CE4F-435B-823E-A02BE59C16D7}.Debug|x64.Build.0 = Debug|Any CPU + {35D47907-CE4F-435B-823E-A02BE59C16D7}.Debug|x86.ActiveCfg = Debug|Any CPU + {35D47907-CE4F-435B-823E-A02BE59C16D7}.Debug|x86.Build.0 = Debug|Any CPU {35D47907-CE4F-435B-823E-A02BE59C16D7}.Product|Any CPU.ActiveCfg = Product|Any CPU {35D47907-CE4F-435B-823E-A02BE59C16D7}.Product|Any CPU.Build.0 = Product|Any CPU + {35D47907-CE4F-435B-823E-A02BE59C16D7}.Product|x64.ActiveCfg = Product|Any CPU + {35D47907-CE4F-435B-823E-A02BE59C16D7}.Product|x64.Build.0 = Product|Any CPU + {35D47907-CE4F-435B-823E-A02BE59C16D7}.Product|x86.ActiveCfg = Product|Any CPU + {35D47907-CE4F-435B-823E-A02BE59C16D7}.Product|x86.Build.0 = Product|Any CPU {35D47907-CE4F-435B-823E-A02BE59C16D7}.Release|Any CPU.ActiveCfg = Release|Any CPU {35D47907-CE4F-435B-823E-A02BE59C16D7}.Release|Any CPU.Build.0 = Release|Any CPU + {35D47907-CE4F-435B-823E-A02BE59C16D7}.Release|x64.ActiveCfg = Release|Any CPU + {35D47907-CE4F-435B-823E-A02BE59C16D7}.Release|x64.Build.0 = Release|Any CPU + {35D47907-CE4F-435B-823E-A02BE59C16D7}.Release|x86.ActiveCfg = Release|Any CPU + {35D47907-CE4F-435B-823E-A02BE59C16D7}.Release|x86.Build.0 = Release|Any CPU {EBC6371C-9454-473D-9547-DF9DECEB2D2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EBC6371C-9454-473D-9547-DF9DECEB2D2A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBC6371C-9454-473D-9547-DF9DECEB2D2A}.Debug|x64.ActiveCfg = Debug|Any CPU + {EBC6371C-9454-473D-9547-DF9DECEB2D2A}.Debug|x64.Build.0 = Debug|Any CPU + {EBC6371C-9454-473D-9547-DF9DECEB2D2A}.Debug|x86.ActiveCfg = Debug|Any CPU + {EBC6371C-9454-473D-9547-DF9DECEB2D2A}.Debug|x86.Build.0 = Debug|Any CPU {EBC6371C-9454-473D-9547-DF9DECEB2D2A}.Product|Any CPU.ActiveCfg = Product|Any CPU {EBC6371C-9454-473D-9547-DF9DECEB2D2A}.Product|Any CPU.Build.0 = Product|Any CPU + {EBC6371C-9454-473D-9547-DF9DECEB2D2A}.Product|x64.ActiveCfg = Product|Any CPU + {EBC6371C-9454-473D-9547-DF9DECEB2D2A}.Product|x64.Build.0 = Product|Any CPU + {EBC6371C-9454-473D-9547-DF9DECEB2D2A}.Product|x86.ActiveCfg = Product|Any CPU + {EBC6371C-9454-473D-9547-DF9DECEB2D2A}.Product|x86.Build.0 = Product|Any CPU {EBC6371C-9454-473D-9547-DF9DECEB2D2A}.Release|Any CPU.ActiveCfg = Release|Any CPU {EBC6371C-9454-473D-9547-DF9DECEB2D2A}.Release|Any CPU.Build.0 = Release|Any CPU + {EBC6371C-9454-473D-9547-DF9DECEB2D2A}.Release|x64.ActiveCfg = Release|Any CPU + {EBC6371C-9454-473D-9547-DF9DECEB2D2A}.Release|x64.Build.0 = Release|Any CPU + {EBC6371C-9454-473D-9547-DF9DECEB2D2A}.Release|x86.ActiveCfg = Release|Any CPU + {EBC6371C-9454-473D-9547-DF9DECEB2D2A}.Release|x86.Build.0 = Release|Any CPU {15272F57-771E-47BE-A960-AD75935254D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {15272F57-771E-47BE-A960-AD75935254D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {15272F57-771E-47BE-A960-AD75935254D0}.Debug|x64.ActiveCfg = Debug|Any CPU + {15272F57-771E-47BE-A960-AD75935254D0}.Debug|x64.Build.0 = Debug|Any CPU + {15272F57-771E-47BE-A960-AD75935254D0}.Debug|x86.ActiveCfg = Debug|Any CPU + {15272F57-771E-47BE-A960-AD75935254D0}.Debug|x86.Build.0 = Debug|Any CPU {15272F57-771E-47BE-A960-AD75935254D0}.Product|Any CPU.ActiveCfg = Product|Any CPU + {15272F57-771E-47BE-A960-AD75935254D0}.Product|x64.ActiveCfg = Product|Any CPU + {15272F57-771E-47BE-A960-AD75935254D0}.Product|x64.Build.0 = Product|Any CPU + {15272F57-771E-47BE-A960-AD75935254D0}.Product|x86.ActiveCfg = Product|Any CPU + {15272F57-771E-47BE-A960-AD75935254D0}.Product|x86.Build.0 = Product|Any CPU {15272F57-771E-47BE-A960-AD75935254D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {15272F57-771E-47BE-A960-AD75935254D0}.Release|x64.ActiveCfg = Release|Any CPU + {15272F57-771E-47BE-A960-AD75935254D0}.Release|x64.Build.0 = Release|Any CPU + {15272F57-771E-47BE-A960-AD75935254D0}.Release|x86.ActiveCfg = Release|Any CPU + {15272F57-771E-47BE-A960-AD75935254D0}.Release|x86.Build.0 = Release|Any CPU {320A245F-6731-476D-A9D8-77888E6B5D9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {320A245F-6731-476D-A9D8-77888E6B5D9C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {320A245F-6731-476D-A9D8-77888E6B5D9C}.Debug|x64.ActiveCfg = Debug|Any CPU + {320A245F-6731-476D-A9D8-77888E6B5D9C}.Debug|x64.Build.0 = Debug|Any CPU + {320A245F-6731-476D-A9D8-77888E6B5D9C}.Debug|x86.ActiveCfg = Debug|Any CPU + {320A245F-6731-476D-A9D8-77888E6B5D9C}.Debug|x86.Build.0 = Debug|Any CPU {320A245F-6731-476D-A9D8-77888E6B5D9C}.Product|Any CPU.ActiveCfg = Product|Any CPU + {320A245F-6731-476D-A9D8-77888E6B5D9C}.Product|x64.ActiveCfg = Product|Any CPU + {320A245F-6731-476D-A9D8-77888E6B5D9C}.Product|x64.Build.0 = Product|Any CPU + {320A245F-6731-476D-A9D8-77888E6B5D9C}.Product|x86.ActiveCfg = Product|Any CPU + {320A245F-6731-476D-A9D8-77888E6B5D9C}.Product|x86.Build.0 = Product|Any CPU {320A245F-6731-476D-A9D8-77888E6B5D9C}.Release|Any CPU.ActiveCfg = Release|Any CPU {320A245F-6731-476D-A9D8-77888E6B5D9C}.Release|Any CPU.Build.0 = Release|Any CPU + {320A245F-6731-476D-A9D8-77888E6B5D9C}.Release|x64.ActiveCfg = Release|Any CPU + {320A245F-6731-476D-A9D8-77888E6B5D9C}.Release|x64.Build.0 = Release|Any CPU + {320A245F-6731-476D-A9D8-77888E6B5D9C}.Release|x86.ActiveCfg = Release|Any CPU + {320A245F-6731-476D-A9D8-77888E6B5D9C}.Release|x86.Build.0 = Release|Any CPU {21392620-7D0E-44B6-9485-93C57F944C20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {21392620-7D0E-44B6-9485-93C57F944C20}.Debug|Any CPU.Build.0 = Debug|Any CPU + {21392620-7D0E-44B6-9485-93C57F944C20}.Debug|x64.ActiveCfg = Debug|Any CPU + {21392620-7D0E-44B6-9485-93C57F944C20}.Debug|x64.Build.0 = Debug|Any CPU + {21392620-7D0E-44B6-9485-93C57F944C20}.Debug|x86.ActiveCfg = Debug|Any CPU + {21392620-7D0E-44B6-9485-93C57F944C20}.Debug|x86.Build.0 = Debug|Any CPU {21392620-7D0E-44B6-9485-93C57F944C20}.Product|Any CPU.ActiveCfg = Product|Any CPU {21392620-7D0E-44B6-9485-93C57F944C20}.Product|Any CPU.Build.0 = Product|Any CPU + {21392620-7D0E-44B6-9485-93C57F944C20}.Product|x64.ActiveCfg = Product|Any CPU + {21392620-7D0E-44B6-9485-93C57F944C20}.Product|x64.Build.0 = Product|Any CPU + {21392620-7D0E-44B6-9485-93C57F944C20}.Product|x86.ActiveCfg = Product|Any CPU + {21392620-7D0E-44B6-9485-93C57F944C20}.Product|x86.Build.0 = Product|Any CPU {21392620-7D0E-44B6-9485-93C57F944C20}.Release|Any CPU.ActiveCfg = Release|Any CPU {21392620-7D0E-44B6-9485-93C57F944C20}.Release|Any CPU.Build.0 = Release|Any CPU + {21392620-7D0E-44B6-9485-93C57F944C20}.Release|x64.ActiveCfg = Release|Any CPU + {21392620-7D0E-44B6-9485-93C57F944C20}.Release|x64.Build.0 = Release|Any CPU + {21392620-7D0E-44B6-9485-93C57F944C20}.Release|x86.ActiveCfg = Release|Any CPU + {21392620-7D0E-44B6-9485-93C57F944C20}.Release|x86.Build.0 = Release|Any CPU {58C8A6A7-D624-4E32-93B9-16B879405CAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {58C8A6A7-D624-4E32-93B9-16B879405CAA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58C8A6A7-D624-4E32-93B9-16B879405CAA}.Debug|x64.ActiveCfg = Debug|Any CPU + {58C8A6A7-D624-4E32-93B9-16B879405CAA}.Debug|x64.Build.0 = Debug|Any CPU + {58C8A6A7-D624-4E32-93B9-16B879405CAA}.Debug|x86.ActiveCfg = Debug|Any CPU + {58C8A6A7-D624-4E32-93B9-16B879405CAA}.Debug|x86.Build.0 = Debug|Any CPU {58C8A6A7-D624-4E32-93B9-16B879405CAA}.Product|Any CPU.ActiveCfg = Product|Any CPU {58C8A6A7-D624-4E32-93B9-16B879405CAA}.Product|Any CPU.Build.0 = Product|Any CPU + {58C8A6A7-D624-4E32-93B9-16B879405CAA}.Product|x64.ActiveCfg = Product|Any CPU + {58C8A6A7-D624-4E32-93B9-16B879405CAA}.Product|x64.Build.0 = Product|Any CPU + {58C8A6A7-D624-4E32-93B9-16B879405CAA}.Product|x86.ActiveCfg = Product|Any CPU + {58C8A6A7-D624-4E32-93B9-16B879405CAA}.Product|x86.Build.0 = Product|Any CPU {58C8A6A7-D624-4E32-93B9-16B879405CAA}.Release|Any CPU.ActiveCfg = Release|Any CPU {58C8A6A7-D624-4E32-93B9-16B879405CAA}.Release|Any CPU.Build.0 = Release|Any CPU + {58C8A6A7-D624-4E32-93B9-16B879405CAA}.Release|x64.ActiveCfg = Release|Any CPU + {58C8A6A7-D624-4E32-93B9-16B879405CAA}.Release|x64.Build.0 = Release|Any CPU + {58C8A6A7-D624-4E32-93B9-16B879405CAA}.Release|x86.ActiveCfg = Release|Any CPU + {58C8A6A7-D624-4E32-93B9-16B879405CAA}.Release|x86.Build.0 = Release|Any CPU {44CF90C8-76E4-4BD6-A957-E8F7AE019B06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {44CF90C8-76E4-4BD6-A957-E8F7AE019B06}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44CF90C8-76E4-4BD6-A957-E8F7AE019B06}.Debug|x64.ActiveCfg = Debug|Any CPU + {44CF90C8-76E4-4BD6-A957-E8F7AE019B06}.Debug|x64.Build.0 = Debug|Any CPU + {44CF90C8-76E4-4BD6-A957-E8F7AE019B06}.Debug|x86.ActiveCfg = Debug|Any CPU + {44CF90C8-76E4-4BD6-A957-E8F7AE019B06}.Debug|x86.Build.0 = Debug|Any CPU {44CF90C8-76E4-4BD6-A957-E8F7AE019B06}.Product|Any CPU.ActiveCfg = Product|Any CPU {44CF90C8-76E4-4BD6-A957-E8F7AE019B06}.Product|Any CPU.Build.0 = Product|Any CPU + {44CF90C8-76E4-4BD6-A957-E8F7AE019B06}.Product|x64.ActiveCfg = Product|Any CPU + {44CF90C8-76E4-4BD6-A957-E8F7AE019B06}.Product|x64.Build.0 = Product|Any CPU + {44CF90C8-76E4-4BD6-A957-E8F7AE019B06}.Product|x86.ActiveCfg = Product|Any CPU + {44CF90C8-76E4-4BD6-A957-E8F7AE019B06}.Product|x86.Build.0 = Product|Any CPU {44CF90C8-76E4-4BD6-A957-E8F7AE019B06}.Release|Any CPU.ActiveCfg = Release|Any CPU {44CF90C8-76E4-4BD6-A957-E8F7AE019B06}.Release|Any CPU.Build.0 = Release|Any CPU + {44CF90C8-76E4-4BD6-A957-E8F7AE019B06}.Release|x64.ActiveCfg = Release|Any CPU + {44CF90C8-76E4-4BD6-A957-E8F7AE019B06}.Release|x64.Build.0 = Release|Any CPU + {44CF90C8-76E4-4BD6-A957-E8F7AE019B06}.Release|x86.ActiveCfg = Release|Any CPU + {44CF90C8-76E4-4BD6-A957-E8F7AE019B06}.Release|x86.Build.0 = Release|Any CPU {3C74C94F-2FEB-47F7-ABB3-B0C9CBCCC876}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3C74C94F-2FEB-47F7-ABB3-B0C9CBCCC876}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3C74C94F-2FEB-47F7-ABB3-B0C9CBCCC876}.Debug|x64.ActiveCfg = Debug|Any CPU + {3C74C94F-2FEB-47F7-ABB3-B0C9CBCCC876}.Debug|x64.Build.0 = Debug|Any CPU + {3C74C94F-2FEB-47F7-ABB3-B0C9CBCCC876}.Debug|x86.ActiveCfg = Debug|Any CPU + {3C74C94F-2FEB-47F7-ABB3-B0C9CBCCC876}.Debug|x86.Build.0 = Debug|Any CPU {3C74C94F-2FEB-47F7-ABB3-B0C9CBCCC876}.Product|Any CPU.ActiveCfg = Product|Any CPU {3C74C94F-2FEB-47F7-ABB3-B0C9CBCCC876}.Product|Any CPU.Build.0 = Product|Any CPU + {3C74C94F-2FEB-47F7-ABB3-B0C9CBCCC876}.Product|x64.ActiveCfg = Product|Any CPU + {3C74C94F-2FEB-47F7-ABB3-B0C9CBCCC876}.Product|x64.Build.0 = Product|Any CPU + {3C74C94F-2FEB-47F7-ABB3-B0C9CBCCC876}.Product|x86.ActiveCfg = Product|Any CPU + {3C74C94F-2FEB-47F7-ABB3-B0C9CBCCC876}.Product|x86.Build.0 = Product|Any CPU {3C74C94F-2FEB-47F7-ABB3-B0C9CBCCC876}.Release|Any CPU.ActiveCfg = Release|Any CPU {3C74C94F-2FEB-47F7-ABB3-B0C9CBCCC876}.Release|Any CPU.Build.0 = Release|Any CPU + {3C74C94F-2FEB-47F7-ABB3-B0C9CBCCC876}.Release|x64.ActiveCfg = Release|Any CPU + {3C74C94F-2FEB-47F7-ABB3-B0C9CBCCC876}.Release|x64.Build.0 = Release|Any CPU + {3C74C94F-2FEB-47F7-ABB3-B0C9CBCCC876}.Release|x86.ActiveCfg = Release|Any CPU + {3C74C94F-2FEB-47F7-ABB3-B0C9CBCCC876}.Release|x86.Build.0 = Release|Any CPU {9AC9AF60-280A-4871-A7FA-69AB4D0C858A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9AC9AF60-280A-4871-A7FA-69AB4D0C858A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9AC9AF60-280A-4871-A7FA-69AB4D0C858A}.Debug|x64.ActiveCfg = Debug|Any CPU + {9AC9AF60-280A-4871-A7FA-69AB4D0C858A}.Debug|x64.Build.0 = Debug|Any CPU + {9AC9AF60-280A-4871-A7FA-69AB4D0C858A}.Debug|x86.ActiveCfg = Debug|Any CPU + {9AC9AF60-280A-4871-A7FA-69AB4D0C858A}.Debug|x86.Build.0 = Debug|Any CPU {9AC9AF60-280A-4871-A7FA-69AB4D0C858A}.Product|Any CPU.ActiveCfg = Product|Any CPU + {9AC9AF60-280A-4871-A7FA-69AB4D0C858A}.Product|x64.ActiveCfg = Product|Any CPU + {9AC9AF60-280A-4871-A7FA-69AB4D0C858A}.Product|x64.Build.0 = Product|Any CPU + {9AC9AF60-280A-4871-A7FA-69AB4D0C858A}.Product|x86.ActiveCfg = Product|Any CPU + {9AC9AF60-280A-4871-A7FA-69AB4D0C858A}.Product|x86.Build.0 = Product|Any CPU {9AC9AF60-280A-4871-A7FA-69AB4D0C858A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9AC9AF60-280A-4871-A7FA-69AB4D0C858A}.Release|x64.ActiveCfg = Release|Any CPU + {9AC9AF60-280A-4871-A7FA-69AB4D0C858A}.Release|x64.Build.0 = Release|Any CPU + {9AC9AF60-280A-4871-A7FA-69AB4D0C858A}.Release|x86.ActiveCfg = Release|Any CPU + {9AC9AF60-280A-4871-A7FA-69AB4D0C858A}.Release|x86.Build.0 = Release|Any CPU {DE2DD6A4-A906-4BA6-8AAA-2A0433DF523F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DE2DD6A4-A906-4BA6-8AAA-2A0433DF523F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE2DD6A4-A906-4BA6-8AAA-2A0433DF523F}.Debug|x64.ActiveCfg = Debug|Any CPU + {DE2DD6A4-A906-4BA6-8AAA-2A0433DF523F}.Debug|x64.Build.0 = Debug|Any CPU + {DE2DD6A4-A906-4BA6-8AAA-2A0433DF523F}.Debug|x86.ActiveCfg = Debug|Any CPU + {DE2DD6A4-A906-4BA6-8AAA-2A0433DF523F}.Debug|x86.Build.0 = Debug|Any CPU {DE2DD6A4-A906-4BA6-8AAA-2A0433DF523F}.Product|Any CPU.ActiveCfg = Product|Any CPU + {DE2DD6A4-A906-4BA6-8AAA-2A0433DF523F}.Product|x64.ActiveCfg = Product|Any CPU + {DE2DD6A4-A906-4BA6-8AAA-2A0433DF523F}.Product|x64.Build.0 = Product|Any CPU + {DE2DD6A4-A906-4BA6-8AAA-2A0433DF523F}.Product|x86.ActiveCfg = Product|Any CPU + {DE2DD6A4-A906-4BA6-8AAA-2A0433DF523F}.Product|x86.Build.0 = Product|Any CPU {DE2DD6A4-A906-4BA6-8AAA-2A0433DF523F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE2DD6A4-A906-4BA6-8AAA-2A0433DF523F}.Release|x64.ActiveCfg = Release|Any CPU + {DE2DD6A4-A906-4BA6-8AAA-2A0433DF523F}.Release|x64.Build.0 = Release|Any CPU + {DE2DD6A4-A906-4BA6-8AAA-2A0433DF523F}.Release|x86.ActiveCfg = Release|Any CPU + {DE2DD6A4-A906-4BA6-8AAA-2A0433DF523F}.Release|x86.Build.0 = Release|Any CPU {73261A8C-FB41-4C4C-90D4-ED5EEC991413}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {73261A8C-FB41-4C4C-90D4-ED5EEC991413}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73261A8C-FB41-4C4C-90D4-ED5EEC991413}.Debug|x64.ActiveCfg = Debug|Any CPU + {73261A8C-FB41-4C4C-90D4-ED5EEC991413}.Debug|x64.Build.0 = Debug|Any CPU + {73261A8C-FB41-4C4C-90D4-ED5EEC991413}.Debug|x86.ActiveCfg = Debug|Any CPU + {73261A8C-FB41-4C4C-90D4-ED5EEC991413}.Debug|x86.Build.0 = Debug|Any CPU {73261A8C-FB41-4C4C-90D4-ED5EEC991413}.Product|Any CPU.ActiveCfg = Product|Any CPU + {73261A8C-FB41-4C4C-90D4-ED5EEC991413}.Product|x64.ActiveCfg = Product|Any CPU + {73261A8C-FB41-4C4C-90D4-ED5EEC991413}.Product|x64.Build.0 = Product|Any CPU + {73261A8C-FB41-4C4C-90D4-ED5EEC991413}.Product|x86.ActiveCfg = Product|Any CPU + {73261A8C-FB41-4C4C-90D4-ED5EEC991413}.Product|x86.Build.0 = Product|Any CPU {73261A8C-FB41-4C4C-90D4-ED5EEC991413}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73261A8C-FB41-4C4C-90D4-ED5EEC991413}.Release|x64.ActiveCfg = Release|Any CPU + {73261A8C-FB41-4C4C-90D4-ED5EEC991413}.Release|x64.Build.0 = Release|Any CPU + {73261A8C-FB41-4C4C-90D4-ED5EEC991413}.Release|x86.ActiveCfg = Release|Any CPU + {73261A8C-FB41-4C4C-90D4-ED5EEC991413}.Release|x86.Build.0 = Release|Any CPU {A20861A9-411E-6150-BF5C-69E8196E5D22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A20861A9-411E-6150-BF5C-69E8196E5D22}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A20861A9-411E-6150-BF5C-69E8196E5D22}.Debug|x64.ActiveCfg = Debug|Any CPU + {A20861A9-411E-6150-BF5C-69E8196E5D22}.Debug|x64.Build.0 = Debug|Any CPU + {A20861A9-411E-6150-BF5C-69E8196E5D22}.Debug|x86.ActiveCfg = Debug|Any CPU + {A20861A9-411E-6150-BF5C-69E8196E5D22}.Debug|x86.Build.0 = Debug|Any CPU {A20861A9-411E-6150-BF5C-69E8196E5D22}.Product|Any CPU.ActiveCfg = Release|Any CPU {A20861A9-411E-6150-BF5C-69E8196E5D22}.Product|Any CPU.Build.0 = Release|Any CPU + {A20861A9-411E-6150-BF5C-69E8196E5D22}.Product|x64.ActiveCfg = Product|Any CPU + {A20861A9-411E-6150-BF5C-69E8196E5D22}.Product|x64.Build.0 = Product|Any CPU + {A20861A9-411E-6150-BF5C-69E8196E5D22}.Product|x86.ActiveCfg = Product|Any CPU + {A20861A9-411E-6150-BF5C-69E8196E5D22}.Product|x86.Build.0 = Product|Any CPU {A20861A9-411E-6150-BF5C-69E8196E5D22}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A20861A9-411E-6150-BF5C-69E8196E5D22}.Release|x64.ActiveCfg = Release|Any CPU + {A20861A9-411E-6150-BF5C-69E8196E5D22}.Release|x64.Build.0 = Release|Any CPU + {A20861A9-411E-6150-BF5C-69E8196E5D22}.Release|x86.ActiveCfg = Release|Any CPU + {A20861A9-411E-6150-BF5C-69E8196E5D22}.Release|x86.Build.0 = Release|Any CPU {B8443014-1247-FB9C-7BF4-2CC944075A8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B8443014-1247-FB9C-7BF4-2CC944075A8B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8443014-1247-FB9C-7BF4-2CC944075A8B}.Debug|x64.ActiveCfg = Debug|Any CPU + {B8443014-1247-FB9C-7BF4-2CC944075A8B}.Debug|x64.Build.0 = Debug|Any CPU + {B8443014-1247-FB9C-7BF4-2CC944075A8B}.Debug|x86.ActiveCfg = Debug|Any CPU + {B8443014-1247-FB9C-7BF4-2CC944075A8B}.Debug|x86.Build.0 = Debug|Any CPU {B8443014-1247-FB9C-7BF4-2CC944075A8B}.Product|Any CPU.ActiveCfg = Product|Any CPU {B8443014-1247-FB9C-7BF4-2CC944075A8B}.Product|Any CPU.Build.0 = Product|Any CPU + {B8443014-1247-FB9C-7BF4-2CC944075A8B}.Product|x64.ActiveCfg = Product|Any CPU + {B8443014-1247-FB9C-7BF4-2CC944075A8B}.Product|x64.Build.0 = Product|Any CPU + {B8443014-1247-FB9C-7BF4-2CC944075A8B}.Product|x86.ActiveCfg = Product|Any CPU + {B8443014-1247-FB9C-7BF4-2CC944075A8B}.Product|x86.Build.0 = Product|Any CPU {B8443014-1247-FB9C-7BF4-2CC944075A8B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8443014-1247-FB9C-7BF4-2CC944075A8B}.Release|x64.ActiveCfg = Release|Any CPU + {B8443014-1247-FB9C-7BF4-2CC944075A8B}.Release|x64.Build.0 = Release|Any CPU + {B8443014-1247-FB9C-7BF4-2CC944075A8B}.Release|x86.ActiveCfg = Release|Any CPU + {B8443014-1247-FB9C-7BF4-2CC944075A8B}.Release|x86.Build.0 = Release|Any CPU + {4A817897-80A8-4F42-86C5-20447401E0AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A817897-80A8-4F42-86C5-20447401E0AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A817897-80A8-4F42-86C5-20447401E0AA}.Debug|x64.ActiveCfg = Debug|Any CPU + {4A817897-80A8-4F42-86C5-20447401E0AA}.Debug|x64.Build.0 = Debug|Any CPU + {4A817897-80A8-4F42-86C5-20447401E0AA}.Debug|x86.ActiveCfg = Debug|Any CPU + {4A817897-80A8-4F42-86C5-20447401E0AA}.Debug|x86.Build.0 = Debug|Any CPU + {4A817897-80A8-4F42-86C5-20447401E0AA}.Product|Any CPU.ActiveCfg = Debug|Any CPU + {4A817897-80A8-4F42-86C5-20447401E0AA}.Product|Any CPU.Build.0 = Debug|Any CPU + {4A817897-80A8-4F42-86C5-20447401E0AA}.Product|x64.ActiveCfg = Debug|Any CPU + {4A817897-80A8-4F42-86C5-20447401E0AA}.Product|x64.Build.0 = Debug|Any CPU + {4A817897-80A8-4F42-86C5-20447401E0AA}.Product|x86.ActiveCfg = Debug|Any CPU + {4A817897-80A8-4F42-86C5-20447401E0AA}.Product|x86.Build.0 = Debug|Any CPU + {4A817897-80A8-4F42-86C5-20447401E0AA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A817897-80A8-4F42-86C5-20447401E0AA}.Release|Any CPU.Build.0 = Release|Any CPU + {4A817897-80A8-4F42-86C5-20447401E0AA}.Release|x64.ActiveCfg = Release|Any CPU + {4A817897-80A8-4F42-86C5-20447401E0AA}.Release|x64.Build.0 = Release|Any CPU + {4A817897-80A8-4F42-86C5-20447401E0AA}.Release|x86.ActiveCfg = Release|Any CPU + {4A817897-80A8-4F42-86C5-20447401E0AA}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/AyCode.Core/AyCode.Core.csproj b/AyCode.Core/AyCode.Core.csproj index 5a7c5eb..52a662e 100644 --- a/AyCode.Core/AyCode.Core.csproj +++ b/AyCode.Core/AyCode.Core.csproj @@ -7,6 +7,9 @@ + diff --git a/AyCode.Core/Properties/AssemblyInfo.cs b/AyCode.Core/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..70e480e --- /dev/null +++ b/AyCode.Core/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +using System.Runtime.CompilerServices; + +// Make internal types visible to test projects and generated code +[assembly: InternalsVisibleTo("AyCode.Core.Tests")] +[assembly: InternalsVisibleTo("AyCode.Core.Tests.Internal")] +[assembly: InternalsVisibleTo("AyCode.Benchmark")] diff --git a/AyCode.Core/Serializers/Attributes/AcBinarySerializableAttribute.cs b/AyCode.Core/Serializers/Attributes/AcBinarySerializableAttribute.cs new file mode 100644 index 0000000..9acd1d1 --- /dev/null +++ b/AyCode.Core/Serializers/Attributes/AcBinarySerializableAttribute.cs @@ -0,0 +1,17 @@ +using System; + +namespace AyCode.Core.Serializers.Attributes; + +/// +/// Marks a class or struct for Source Generator based binary serialization. +/// When applied, the Source Generator will create optimized serialize/deserialize methods +/// at compile time, eliminating the need for runtime reflection or compiled expressions. +/// +/// +/// If this attribute is not present, the serializer falls back to the existing +/// compiled expression based approach which works for all types. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false, AllowMultiple = false)] +public sealed class AcBinarySerializableAttribute : Attribute +{ +} diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs index a2313e8..7d222e6 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs @@ -9,6 +9,9 @@ namespace AyCode.Core.Serializers.Binaries; public static partial class AcBinaryDeserializer { + /// + /// Binary deserialization context. Public for generated serializers. + /// internal ref struct BinaryDeserializationContext { private readonly ReadOnlySpan _buffer; diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs index f8f2723..cd923c4 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs @@ -1,8 +1,7 @@ -using System.Collections; -using System.Collections.Frozen; using System.Reflection; using System.Runtime.CompilerServices; using AyCode.Core.Serializers; +using AyCode.Core.Serializers.Attributes; namespace AyCode.Core.Serializers.Binaries; @@ -16,6 +15,19 @@ public static partial class AcBinaryDeserializer /// public BinaryPropertySetterInfo[] PropertiesArray { get; } + /// + /// True if this type has a Source Generator generated deserializer available. + /// Note: Due to ref struct limitations, the generated code cannot be called via delegates. + /// The generated code must be called directly from consumer code that has access to it. + /// + public bool HasGeneratedDeserializer { get; } + + /// + /// The generated serializer type, if available. + /// Consumer code can use this to call the generated methods directly. + /// + public Type? GeneratedSerializerType { get; } + public BinaryDeserializeTypeMetadata(Type type) : base(type) { var orderedProperties = GetSerializableProperties(type, requiresRead: true, requiresWrite: true); @@ -25,6 +37,18 @@ public static partial class AcBinaryDeserializer { PropertiesArray[i] = new BinaryPropertySetterInfo(orderedProperties[i], type); } + + // Fast check: only look for generated serializer if type has [AcBinarySerializable] attribute + if (type.IsDefined(typeof(AcBinarySerializableAttribute), inherit: false)) + { + GeneratedSerializerType = FindGeneratedSerializerType(type); + HasGeneratedDeserializer = GeneratedSerializerType != null; + } + else + { + GeneratedSerializerType = null; + HasGeneratedDeserializer = false; + } } /// @@ -33,6 +57,28 @@ public static partial class AcBinaryDeserializer [MethodImpl(MethodImplOptions.AggressiveInlining)] public BinaryPropertySetterInfo? GetPropertyByIndex(int index) => (uint)index < (uint)PropertiesArray.Length ? PropertiesArray[index] : null; + + /// + /// Finds the generated serializer type for the given type. + /// + private static Type? FindGeneratedSerializerType(Type type) + { + // Generated class name format: {ClassName}_AcBinarySerializer + var generatedTypeName = $"{type.FullName}_AcBinarySerializer"; + + // Try to find in the same assembly + var serializerType = type.Assembly.GetType(generatedTypeName); + + // For nested types, the generated class is at namespace level + if (serializerType == null && type.IsNested) + { + var ns = type.Namespace ?? ""; + var simpleName = $"{(string.IsNullOrEmpty(ns) ? "" : ns + ".")}{type.Name}_AcBinarySerializer"; + serializerType = type.Assembly.GetType(simpleName); + } + + return serializerType; + } } /// diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index eef28b1..134bdad 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -44,6 +44,9 @@ public static partial class AcBinarySerializer } } + /// + /// Binary serialization context. Public for generated serializers. + /// internal sealed class BinarySerializationContext : IDisposable { private const int MinBufferSize = 256; diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs index 8f80ce7..31999b6 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs @@ -1,6 +1,8 @@ using System; using System.Reflection; +using System.Runtime.CompilerServices; using AyCode.Core.Serializers; +using AyCode.Core.Serializers.Attributes; using static AyCode.Core.Helpers.JsonUtilities; namespace AyCode.Core.Serializers.Binaries; @@ -11,11 +13,18 @@ public static partial class AcBinarySerializer { public BinaryPropertyAccessor[] Properties { get; } + /// + /// True if this type has a Source Generator generated serializer available. + /// + public bool HasGeneratedSerializer { get; } + + /// + /// The generated serializer type, if available. + /// + public Type? GeneratedSerializerType { get; } + public BinaryTypeMetadata(Type type) : base(type) { - // Use requiresWrite: true to match deserializer's property list - // This ensures read-only properties (like computed properties) are excluded - // and property indices are consistent between serialization and deserialization var orderedProperties = GetSerializableProperties(type, requiresRead: true, requiresWrite: true); Properties = new BinaryPropertyAccessor[orderedProperties.Length]; @@ -25,6 +34,29 @@ public static partial class AcBinarySerializer accessor.PropertyIndex = i; Properties[i] = accessor; } + + // Fast check: only look for generated serializer if type has [AcBinarySerializable] attribute + if (type.IsDefined(typeof(AcBinarySerializableAttribute), inherit: false)) + { + GeneratedSerializerType = FindGeneratedSerializerType(type); + HasGeneratedSerializer = GeneratedSerializerType != null; + } + } + + private static Type? FindGeneratedSerializerType(Type type) + { + var generatedTypeName = $"{type.FullName}_AcBinarySerializer"; + + var serializerType = type.Assembly.GetType(generatedTypeName); + + if (serializerType == null && type.IsNested) + { + var ns = type.Namespace ?? ""; + var simpleName = $"{(string.IsNullOrEmpty(ns) ? "" : ns + ".")}{type.Name}_AcBinarySerializer"; + serializerType = type.Assembly.GetType(simpleName); + } + + return serializerType; } }