diff --git a/AyCode.Core.Server/AyCode.Core.Server.csproj b/AyCode.Core.Server/AyCode.Core.Server.csproj
index 54de42a..5364cf2 100644
--- a/AyCode.Core.Server/AyCode.Core.Server.csproj
+++ b/AyCode.Core.Server/AyCode.Core.Server.csproj
@@ -7,6 +7,7 @@
+
diff --git a/AyCode.Core.sln b/AyCode.Core.sln
index 7e33393..02490a2 100644
--- a/AyCode.Core.sln
+++ b/AyCode.Core.sln
@@ -44,6 +44,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
AyCode.Core.targets = AyCode.Core.targets
RunQuickBenchmark.bat = RunQuickBenchmark.bat
RunQuickBenchmark.ps1 = RunQuickBenchmark.ps1
+ ToonExtendedInfo.txt = ToonExtendedInfo.txt
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AyCode.Benchmark", "AyCode.Benchmark\AyCode.Benchmark.csproj", "{A20861A9-411E-6150-BF5C-69E8196E5D22}"
diff --git a/AyCode.Core/AyCode.Core.csproj b/AyCode.Core/AyCode.Core.csproj
index 52a662e..4983021 100644
--- a/AyCode.Core/AyCode.Core.csproj
+++ b/AyCode.Core/AyCode.Core.csproj
@@ -17,6 +17,7 @@
+
diff --git a/AyCode.Core/Compression/BrotliHelper.cs b/AyCode.Core/Compression/BrotliHelper.cs
index 2a0dd0b..ea6b2c3 100644
--- a/AyCode.Core/Compression/BrotliHelper.cs
+++ b/AyCode.Core/Compression/BrotliHelper.cs
@@ -1,3 +1,4 @@
+using AyCode.Core.Serializers.Toons;
using System.Buffers;
using System.IO.Compression;
using System.Runtime.CompilerServices;
@@ -11,6 +12,7 @@ namespace AyCode.Core.Compression;
///
public static class BrotliHelper
{
+ //[ToonDescription("Unique identifier for the person")]
private const int DefaultBufferSize = 4096;
private const int MaxStackAllocSize = 1024;
diff --git a/AyCode.Core/Loggers/AcLoggerAdapter.cs b/AyCode.Core/Loggers/AcLoggerAdapter.cs
new file mode 100644
index 0000000..f958538
--- /dev/null
+++ b/AyCode.Core/Loggers/AcLoggerAdapter.cs
@@ -0,0 +1,75 @@
+using System.Collections.Concurrent;
+using Microsoft.Extensions.Logging;
+
+namespace AyCode.Core.Loggers;
+
+///
+/// ILoggerProvider implementation that creates logger instances using AcLoggerBase.
+/// Since AcLoggerBase now implements ILogger directly, this provider just wraps
+/// the base logger with category-specific instances.
+///
+public sealed class AcLoggerProvider : ILoggerProvider where TLogger : AcLoggerBase
+{
+ private readonly Func _loggerFactory;
+ private readonly ConcurrentDictionary _loggers = new(StringComparer.OrdinalIgnoreCase);
+
+ ///
+ /// Creates a provider that uses a factory function to create category-specific loggers.
+ ///
+ /// Factory function that creates a logger for a given category name.
+ public AcLoggerProvider(Func loggerFactory)
+ {
+ ArgumentNullException.ThrowIfNull(loggerFactory);
+ _loggerFactory = loggerFactory;
+ }
+
+ public ILogger CreateLogger(string categoryName)
+ {
+ return _loggers.GetOrAdd(categoryName, _loggerFactory);
+ }
+
+ public void Dispose()
+ {
+ _loggers.Clear();
+ }
+}
+
+///
+/// Extension methods for registering AcLogger with Microsoft's DI and logging infrastructure.
+///
+public static class AcLoggerExtensions
+{
+ ///
+ /// Adds AcLogger as a logging provider using a factory function.
+ /// The factory receives the category name and should return a configured logger instance.
+ ///
+ ///
+ ///
+ /// builder.Logging.AddAcLogger(categoryName => new MyLogger(categoryName));
+ ///
+ ///
+ public static ILoggingBuilder AddAcLogger(this ILoggingBuilder builder, Func loggerFactory)
+ where TLogger : AcLoggerBase
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(loggerFactory);
+
+ builder.AddProvider(new AcLoggerProvider(loggerFactory));
+ return builder;
+ }
+
+ ///
+ /// Clears default providers and adds only AcLogger.
+ /// Use this if you want ONLY your logger, not Microsoft's console/debug loggers.
+ ///
+ public static ILoggingBuilder UseOnlyAcLogger(this ILoggingBuilder builder, Func loggerFactory)
+ where TLogger : AcLoggerBase
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(loggerFactory);
+
+ builder.ClearProviders();
+ builder.AddProvider(new AcLoggerProvider(loggerFactory));
+ return builder;
+ }
+}
diff --git a/AyCode.Core/Loggers/AcLoggerBase.cs b/AyCode.Core/Loggers/AcLoggerBase.cs
index 70188ac..17f0dbe 100644
--- a/AyCode.Core/Loggers/AcLoggerBase.cs
+++ b/AyCode.Core/Loggers/AcLoggerBase.cs
@@ -4,7 +4,10 @@ using System.Security.AccessControl;
using AyCode.Core.Consts;
using AyCode.Core.Enums;
using AyCode.Utils.Extensions;
+using Microsoft.Extensions.Logging;
using static System.Net.Mime.MediaTypeNames;
+using AcLogLevel = AyCode.Core.Loggers.LogLevel;
+using MsLogLevel = Microsoft.Extensions.Logging.LogLevel;
namespace AyCode.Core.Loggers;
@@ -12,11 +15,18 @@ public abstract class AcLoggerBase : IAcLoggerBase
{
protected readonly List LogWriters = [];
- public LogLevel LogLevel { get; set; } = LogLevel.Error;
+ public AcLogLevel LogLevel { get; set; } = AcLogLevel.Error;
public AppType AppType { get; set; } = AppType.Server;
public string? CategoryName { get; set; }
+ ///
+ /// If true, long category names (e.g., "Microsoft.AspNetCore.SignalR.HubConnectionHandler")
+ /// will be shortened to just the class name (e.g., "HubConnectionHandler").
+ /// Default is true for consistency with typeof(T).Name usage.
+ ///
+ public bool ShortenCategoryNames { get; set; } = true;
+
protected AcLoggerBase() : this(null)
{
}
@@ -26,7 +36,7 @@ public abstract class AcLoggerBase : IAcLoggerBase
CategoryName = categoryName ?? "...";
AppType = AcEnv.AppConfiguration.GetEnum("AyCode:Logger:AppType");
- LogLevel = AcEnv.AppConfiguration.GetEnum("AyCode:Logger:LogLevel");
+ LogLevel = AcEnv.AppConfiguration.GetEnum("AyCode:Logger:LogLevel");
foreach (var logWriterSection in AcEnv.AppConfiguration.GetSection("AyCode:Logger:LogWriters").GetChildren())
{
@@ -41,7 +51,7 @@ public abstract class AcLoggerBase : IAcLoggerBase
continue;
}
- var logWriterLogLevel = logWriterSection.GetEnum("LogLevel");
+ var logWriterLogLevel = logWriterSection.GetEnum("LogLevel");
if (Activator.CreateInstance(logWriterType, AppType, logWriterLogLevel, CategoryName) is IAcLogWriterBase logWriter) LogWriters.Add(logWriter);
else Console.Error.WriteLine($"{GetType().Name}; Can't create logWriterType instance; logWriterType: {logWriterType?.AssemblyQualifiedName};");
@@ -54,11 +64,11 @@ public abstract class AcLoggerBase : IAcLoggerBase
}
protected AcLoggerBase(string? categoryName, params IAcLogWriterBase[] logWriters) :
- this(AcEnv.AppConfiguration.GetEnum("AyCode:Logger:AppType"), AcEnv.AppConfiguration.GetEnum("AyCode:Logger:LogLevel"), categoryName, logWriters)
+ this(AcEnv.AppConfiguration.GetEnum("AyCode:Logger:AppType"), AcEnv.AppConfiguration.GetEnum("AyCode:Logger:LogLevel"), categoryName, logWriters)
{
}
- protected AcLoggerBase(AppType appType, LogLevel logLevel, string? categoryName, params IAcLogWriterBase[] logWriters)
+ protected AcLoggerBase(AppType appType, AcLogLevel logLevel, string? categoryName, params IAcLogWriterBase[] logWriters)
{
AppType = appType;
LogLevel = logLevel;
@@ -72,9 +82,11 @@ public abstract class AcLoggerBase : IAcLoggerBase
public List GetWriters => [.. LogWriters];
public TLogWriter? Writer() where TLogWriter : IAcLogWriterBase => LogWriters.OfType().FirstOrDefault();
+ #region IAcLogWriterBase Implementation
+
public virtual void Detail(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
{
- if (LogLevel <= LogLevel.Detail) LogWriters.ForEach(x => x.Detail(text, categoryName ?? CategoryName, memberName));
+ if (LogLevel <= AcLogLevel.Detail) LogWriters.ForEach(x => x.Detail(text, categoryName ?? CategoryName, memberName));
}
[Conditional("DEBUG")]
@@ -83,7 +95,7 @@ public abstract class AcLoggerBase : IAcLoggerBase
public virtual void Debug(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
{
- if (LogLevel <= LogLevel.Debug) LogWriters.ForEach(x => x.Debug(text, categoryName ?? CategoryName, memberName));
+ if (LogLevel <= AcLogLevel.Debug) LogWriters.ForEach(x => x.Debug(text, categoryName ?? CategoryName, memberName));
}
[Conditional("DEBUG")]
@@ -92,7 +104,7 @@ public abstract class AcLoggerBase : IAcLoggerBase
public virtual void Info(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
{
- if (LogLevel <= LogLevel.Info) LogWriters.ForEach(x => x.Info(text, categoryName ?? CategoryName, memberName));
+ if (LogLevel <= AcLogLevel.Info) LogWriters.ForEach(x => x.Info(text, categoryName ?? CategoryName, memberName));
}
[Conditional("DEBUG")]
@@ -101,7 +113,7 @@ public abstract class AcLoggerBase : IAcLoggerBase
public virtual void Warning(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
{
- if (LogLevel <= LogLevel.Warning) LogWriters.ForEach(x => x.Warning(text, categoryName ?? CategoryName, memberName));
+ if (LogLevel <= AcLogLevel.Warning) LogWriters.ForEach(x => x.Warning(text, categoryName ?? CategoryName, memberName));
}
[Conditional("DEBUG")]
@@ -110,7 +122,7 @@ public abstract class AcLoggerBase : IAcLoggerBase
public virtual void Suggest(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
{
- if (LogLevel <= LogLevel.Suggest) LogWriters.ForEach(x => x.Suggest(text, categoryName ?? CategoryName, memberName));
+ if (LogLevel <= AcLogLevel.Suggest) LogWriters.ForEach(x => x.Suggest(text, categoryName ?? CategoryName, memberName));
}
[Conditional("DEBUG")]
@@ -119,37 +131,36 @@ public abstract class AcLoggerBase : IAcLoggerBase
public virtual void Error(string? text, Exception? ex = null, string? categoryName = null, [CallerMemberName] string? memberName = null)
{
- if (LogLevel <= LogLevel.Error) LogWriters.ForEach(x => x.Error(text, ex, categoryName ?? CategoryName, memberName));
+ if (LogLevel <= AcLogLevel.Error) LogWriters.ForEach(x => x.Error(text, ex, categoryName ?? CategoryName, memberName));
}
[Conditional("DEBUG")]
public void ErrorConditional(string? text, Exception? ex = null, string? categoryName = null, [CallerMemberName] string? memberName = null)
=> Error(text, ex, categoryName, memberName);
- public void Write(AppType appType, LogLevel logLevel, string? logText, string? callerMemberName, string? categoryName)
+ public void Write(AppType appType, AcLogLevel logLevel, string? logText, string? callerMemberName, string? categoryName)
=> Write(appType, logLevel, logText, callerMemberName, categoryName, null, null);
[Conditional("DEBUG")]
- public void WriteConditional(AppType appType, LogLevel logLevel, string? logText, string? callerMemberName, string? categoryName)
+ public void WriteConditional(AppType appType, AcLogLevel logLevel, string? logText, string? callerMemberName, string? categoryName)
=> Write(appType, logLevel, logText, callerMemberName, categoryName, null, null);
- public void Write(AppType appType, LogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, Exception? ex)
+ public void Write(AppType appType, AcLogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, Exception? ex)
=> Write(appType, logLevel, logText, callerMemberName, categoryName, ex?.GetType().Name, ex?.ToString());
[Conditional("DEBUG")]
- public void WriteConditional(AppType appType, LogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, Exception? ex)
+ public void WriteConditional(AppType appType, AcLogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, Exception? ex)
=> Write(appType, logLevel, logText, callerMemberName, categoryName, ex);
- public virtual void Write(AppType appType, LogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, string? errorType, string? exMessage)
+ public virtual void Write(AppType appType, AcLogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, string? errorType, string? exMessage)
{
if (LogLevel <= logLevel) LogWriters.ForEach(x => x.Write(appType, logLevel, logText, callerMemberName, categoryName ?? CategoryName, errorType, exMessage));
}
[Conditional("DEBUG")]
- public void WriteConditional(AppType appType, LogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, string? errorType, string? exMessage)
+ public void WriteConditional(AppType appType, AcLogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, string? errorType, string? exMessage)
=> Write(appType, logLevel, logText, callerMemberName, categoryName, errorType, exMessage);
-
public void Write(IAcLogItemClient logItem)
{
if (LogLevel <= logItem.LogLevel) LogWriters.ForEach(x => x.Write(logItem));
@@ -157,4 +168,114 @@ public abstract class AcLoggerBase : IAcLoggerBase
[Conditional("DEBUG")]
public void WriteConditional(IAcLogItemClient logItem) => Write(logItem);
+
+ #endregion
+
+ #region ILogger Implementation
+
+ ///
+ /// ILogger.BeginScope - AcLoggerBase doesn't support scopes, returns no-op disposable.
+ ///
+ public IDisposable? BeginScope(TState state) where TState : notnull => NullScope.Instance;
+
+ ///
+ /// ILogger.IsEnabled - Checks if the specified Microsoft log level is enabled.
+ ///
+ public bool IsEnabled(MsLogLevel logLevel)
+ {
+ var acLogLevel = MapFromMsLogLevel(logLevel);
+ return LogLevel <= acLogLevel;
+ }
+
+ ///
+ /// ILogger.Log - Main logging method called by Microsoft services.
+ ///
+ public void Log(MsLogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter)
+ {
+ if (!IsEnabled(logLevel))
+ return;
+
+ var message = formatter(state, exception);
+
+ if (string.IsNullOrEmpty(message) && exception == null)
+ return;
+
+ //var fullMessage = eventId.Id != 0
+ // ? $"[{eventId.Id}:{eventId.Name}] {message}"
+ // : message;
+
+ var fullMessage = message;
+ var category = ShortenCategoryNames ? GetShortCategoryName(CategoryName) : CategoryName;
+
+ // Use eventId.Name as memberName if available, otherwise null (will show as empty, not "Log")
+ var memberName = !string.IsNullOrEmpty(eventId.Name) ? $"{eventId.Name}:{eventId.Id}" : "Log";
+
+ switch (logLevel)
+ {
+ case MsLogLevel.Trace:
+ Detail(fullMessage, category, memberName);
+ break;
+ case MsLogLevel.Debug:
+ Debug(fullMessage, category, memberName);
+ break;
+ case MsLogLevel.Information:
+ Info(fullMessage, category, memberName);
+ break;
+ case MsLogLevel.Warning:
+ Warning(fullMessage, category, memberName);
+ break;
+ case MsLogLevel.Error:
+ Error(fullMessage, exception, category, memberName);
+ break;
+ case MsLogLevel.Critical:
+ Error($"[CRITICAL] {fullMessage}", exception, category, memberName);
+ break;
+ case MsLogLevel.None:
+ default:
+ break;
+ }
+ }
+
+ ///
+ /// Shortens a fully qualified type name to just the class name.
+ /// E.g., "Microsoft.AspNetCore.SignalR.HubConnectionHandler" -> "HubConnectionHandler"
+ ///
+ private static string? GetShortCategoryName(string? categoryName)
+ {
+ if (string.IsNullOrEmpty(categoryName))
+ return categoryName;
+
+ var lastDot = categoryName.LastIndexOf('.');
+ return lastDot >= 0 ? categoryName[(lastDot + 1)..] : categoryName;
+ }
+
+ ///
+ /// Maps Microsoft.Extensions.Logging.LogLevel to AyCode.Core.Loggers.LogLevel.
+ ///
+ private static AcLogLevel MapFromMsLogLevel(MsLogLevel msLogLevel)
+ {
+ return msLogLevel switch
+ {
+ MsLogLevel.Trace => AcLogLevel.Detail,
+ MsLogLevel.Debug => AcLogLevel.Debug,
+ MsLogLevel.Information => AcLogLevel.Info,
+ MsLogLevel.Warning => AcLogLevel.Warning,
+ MsLogLevel.Error => AcLogLevel.Error,
+ MsLogLevel.Critical => AcLogLevel.Error,
+ MsLogLevel.None => AcLogLevel.Disabled,
+ _ => AcLogLevel.Info
+ };
+ }
+
+ ///
+ /// No-op scope implementation for ILogger.BeginScope.
+ ///
+ private sealed class NullScope : IDisposable
+ {
+ public static NullScope Instance { get; } = new();
+ private NullScope() { }
+ public void Dispose() { }
+ }
+
+ #endregion
}
\ No newline at end of file
diff --git a/AyCode.Core/Loggers/IAcLoggerBase.cs b/AyCode.Core/Loggers/IAcLoggerBase.cs
index 16a5715..d558300 100644
--- a/AyCode.Core/Loggers/IAcLoggerBase.cs
+++ b/AyCode.Core/Loggers/IAcLoggerBase.cs
@@ -1,6 +1,8 @@
-namespace AyCode.Core.Loggers;
+using Microsoft.Extensions.Logging;
-public interface IAcLoggerBase : IAcLogWriterBase
+namespace AyCode.Core.Loggers;
+
+public interface IAcLoggerBase : IAcLogWriterBase, ILogger
{
public List GetWriters { get; }
public TLogWriter? Writer() where TLogWriter : IAcLogWriterBase;
diff --git a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs
index 2583bbc..67215bb 100644
--- a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs
+++ b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs
@@ -1,5 +1,8 @@
+using System.Buffers;
using System.Collections;
+using System.Collections.Concurrent;
using System.Globalization;
+using System.Linq;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.Text;
diff --git a/AyCode.Core/Serializers/Jsons/AcJsonSerializerOptions.cs b/AyCode.Core/Serializers/Jsons/AcJsonSerializerOptions.cs
index 72be84b..9a297d7 100644
--- a/AyCode.Core/Serializers/Jsons/AcJsonSerializerOptions.cs
+++ b/AyCode.Core/Serializers/Jsons/AcJsonSerializerOptions.cs
@@ -6,6 +6,7 @@ public enum AcSerializerType : byte
{
Json = 0,
Binary = 1,
+ Toon = 2,
}
///
diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.AttributeExtraction.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.AttributeExtraction.cs
new file mode 100644
index 0000000..4a00c4e
--- /dev/null
+++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.AttributeExtraction.cs
@@ -0,0 +1,442 @@
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Reflection;
+using System.Text.RegularExpressions;
+
+namespace AyCode.Core.Serializers.Toons;
+
+public static partial class AcToonSerializer
+{
+ ///
+ /// Extract constraints from C# type system (value types, nullables, numeric ranges).
+ ///
+ private static string ExtractTypeConstraints(Type type)
+ {
+ var constraints = new List();
+
+ var underlyingType = Nullable.GetUnderlyingType(type);
+ var isNullable = underlyingType != null || !type.IsValueType;
+
+ // Nullable vs Required
+ if (isNullable)
+ constraints.Add("nullable");
+ else
+ constraints.Add("required");
+
+ var baseType = underlyingType ?? type;
+
+ // Numeric type ranges
+ var typeCode = Type.GetTypeCode(baseType);
+ switch (typeCode)
+ {
+ case TypeCode.Byte:
+ constraints.Add("range: 0-255");
+ break;
+ case TypeCode.SByte:
+ constraints.Add("range: -128-127");
+ break;
+ case TypeCode.Int16:
+ constraints.Add("range: -32768-32767");
+ break;
+ case TypeCode.UInt16:
+ constraints.Add("range: 0-65535");
+ break;
+ case TypeCode.Int32:
+ constraints.Add("numeric");
+ break;
+ case TypeCode.UInt32:
+ constraints.Add("range: 0-4294967295");
+ break;
+ case TypeCode.Int64:
+ constraints.Add("numeric");
+ break;
+ case TypeCode.UInt64:
+ constraints.Add("numeric, non-negative");
+ break;
+ case TypeCode.Single:
+ case TypeCode.Double:
+ case TypeCode.Decimal:
+ constraints.Add("numeric");
+ break;
+ case TypeCode.Boolean:
+ constraints.Add("boolean: true|false");
+ break;
+ }
+
+ // Enum values
+ if (baseType.IsEnum)
+ {
+ var values = string.Join("|", Enum.GetNames(baseType).Take(5)); // Limit to 5 values
+ var more = Enum.GetNames(baseType).Length > 5 ? "..." : "";
+ constraints.Add($"enum: {values}{more}");
+ }
+
+ return string.Join(", ", constraints);
+ }
+
+ ///
+ /// Extract constraints from Microsoft DataAnnotations attributes.
+ ///
+ private static string ExtractDataAnnotationConstraints(PropertyInfo prop)
+ {
+ var constraints = new List();
+
+ // [Required]
+ if (prop.GetCustomAttribute() != null)
+ constraints.Add("required");
+
+ // [Range]
+ var range = prop.GetCustomAttribute();
+ if (range != null)
+ constraints.Add($"range: {range.Minimum}-{range.Maximum}");
+
+ // [MaxLength]
+ var maxLen = prop.GetCustomAttribute();
+ if (maxLen != null)
+ constraints.Add($"max-length: {maxLen.Length}");
+
+ // [MinLength]
+ var minLen = prop.GetCustomAttribute();
+ if (minLen != null)
+ constraints.Add($"min-length: {minLen.Length}");
+
+ // [StringLength]
+ var strLen = prop.GetCustomAttribute();
+ if (strLen != null)
+ {
+ if (strLen.MinimumLength > 0)
+ constraints.Add($"length: {strLen.MinimumLength}-{strLen.MaximumLength}");
+ else
+ constraints.Add($"max-length: {strLen.MaximumLength}");
+ }
+
+ // [EmailAddress]
+ if (prop.GetCustomAttribute() != null)
+ constraints.Add("email-format");
+
+ // [Phone]
+ if (prop.GetCustomAttribute() != null)
+ constraints.Add("phone-format");
+
+ // [Url]
+ if (prop.GetCustomAttribute() != null)
+ constraints.Add("url-format");
+
+ // [CreditCard]
+ if (prop.GetCustomAttribute() != null)
+ constraints.Add("credit-card-format");
+
+ // [RegularExpression]
+ var regex = prop.GetCustomAttribute();
+ if (regex != null)
+ constraints.Add($"pattern: {regex.Pattern}");
+
+ return string.Join(", ", constraints);
+ }
+
+ ///
+ /// Merge constraints with priority: custom > ms > inferred > type.
+ /// Handles deduplication and cleanup.
+ ///
+ private static string MergeConstraints(
+ string? typeConstraints,
+ string? msConstraints,
+ string? inferredConstraints,
+ string? customConstraints)
+ {
+ var all = new HashSet();
+
+ // Add in reverse priority order (lower priority first)
+ AddConstraintsToSet(all, typeConstraints);
+ AddConstraintsToSet(all, inferredConstraints);
+ AddConstraintsToSet(all, msConstraints);
+ AddConstraintsToSet(all, customConstraints);
+
+ return string.Join(", ", all.OrderBy(GetConstraintPriority));
+ }
+
+ private static void AddConstraintsToSet(HashSet set, string? constraints)
+ {
+ if (string.IsNullOrWhiteSpace(constraints)) return;
+
+ foreach (var c in constraints.Split(',').Select(x => x.Trim()).Where(x => !string.IsNullOrEmpty(x)))
+ {
+ // Remove duplicates based on constraint type
+ var constraintType = c.Split(':')[0].Trim();
+
+ // Remove existing constraints of same type (e.g., old range before adding new range)
+ set.RemoveWhere(x => x.Split(':')[0].Trim().Equals(constraintType, StringComparison.OrdinalIgnoreCase));
+
+ set.Add(c);
+ }
+ }
+
+ private static int GetConstraintPriority(string constraint)
+ {
+ var lower = constraint.ToLowerInvariant();
+
+ // Priority order
+ if (lower.StartsWith("required")) return 1;
+ if (lower.StartsWith("nullable")) return 2;
+ if (lower.StartsWith("range:")) return 3;
+ if (lower.StartsWith("length:")) return 4;
+ if (lower.StartsWith("max-length:")) return 5;
+ if (lower.StartsWith("min-length:")) return 6;
+ if (lower.Contains("-format")) return 7;
+ if (lower.StartsWith("pattern:")) return 8;
+ if (lower.StartsWith("enum:")) return 9;
+
+ return 100; // Other constraints last
+ }
+
+ ///
+ /// Resolve [#...] placeholders in description string.
+ ///
+ private static string ResolveDescriptionPlaceholders(
+ string template,
+ ToonPropertyAccessor prop,
+ Type declaringType)
+ {
+ var result = template;
+
+ // [#Description] → Microsoft [Description]
+ if (result.Contains("[#Description]"))
+ {
+ var msDesc = prop.PropertyInfo.GetCustomAttribute();
+ var value = msDesc?.Description ?? "";
+ result = result.Replace("[#Description]", value);
+ }
+
+ // [#DisplayName] → Microsoft [DisplayName]
+ if (result.Contains("[#DisplayName]"))
+ {
+ var displayName = prop.PropertyInfo.GetCustomAttribute();
+ var value = displayName?.DisplayName ?? prop.Name;
+ result = result.Replace("[#DisplayName]", value);
+ }
+
+ // [#SmartDescription] → Smart inference
+ if (result.Contains("[#SmartDescription]"))
+ {
+ var value = GetPropertyDescription(declaringType, prop.Name, prop.PropertyType);
+ result = result.Replace("[#SmartDescription]", value);
+ }
+
+ return CleanupPlaceholders(result);
+ }
+
+ ///
+ /// Resolve [#...] placeholders in purpose string.
+ ///
+ private static string ResolvePurposePlaceholders(
+ string template,
+ ToonPropertyAccessor prop,
+ Type declaringType)
+ {
+ var result = template;
+
+ // [#SmartPurpose] → Smart inference
+ if (result.Contains("[#SmartPurpose]"))
+ {
+ var value = GetPropertyPurpose(declaringType, prop.Name);
+ result = result.Replace("[#SmartPurpose]", value);
+ }
+
+ return CleanupPlaceholders(result);
+ }
+
+ ///
+ /// Resolve [#...] placeholders in constraints string.
+ ///
+ private static string ResolveConstraintPlaceholders(
+ string template,
+ ToonPropertyAccessor prop)
+ {
+ var result = template;
+
+ // [#Range]
+ if (result.Contains("[#Range]"))
+ {
+ var attr = prop.PropertyInfo.GetCustomAttribute();
+ var value = attr != null ? $"range: {attr.Minimum}-{attr.Maximum}" : "";
+ result = result.Replace("[#Range]", value);
+ }
+
+ // [#Required]
+ if (result.Contains("[#Required]"))
+ {
+ var value = prop.PropertyInfo.GetCustomAttribute() != null ? "required" : "";
+ result = result.Replace("[#Required]", value);
+ }
+
+ // [#MaxLength]
+ if (result.Contains("[#MaxLength]"))
+ {
+ var attr = prop.PropertyInfo.GetCustomAttribute();
+ var value = attr != null ? $"max-length: {attr.Length}" : "";
+ result = result.Replace("[#MaxLength]", value);
+ }
+
+ // [#MinLength]
+ if (result.Contains("[#MinLength]"))
+ {
+ var attr = prop.PropertyInfo.GetCustomAttribute();
+ var value = attr != null ? $"min-length: {attr.Length}" : "";
+ result = result.Replace("[#MinLength]", value);
+ }
+
+ // [#StringLength]
+ if (result.Contains("[#StringLength]"))
+ {
+ var attr = prop.PropertyInfo.GetCustomAttribute();
+ string value = "";
+ if (attr != null)
+ {
+ value = attr.MinimumLength > 0
+ ? $"length: {attr.MinimumLength}-{attr.MaximumLength}"
+ : $"max-length: {attr.MaximumLength}";
+ }
+ result = result.Replace("[#StringLength]", value);
+ }
+
+ // [#EmailAddress]
+ if (result.Contains("[#EmailAddress]"))
+ {
+ var value = prop.PropertyInfo.GetCustomAttribute() != null ? "email-format" : "";
+ result = result.Replace("[#EmailAddress]", value);
+ }
+
+ // [#Phone]
+ if (result.Contains("[#Phone]"))
+ {
+ var value = prop.PropertyInfo.GetCustomAttribute() != null ? "phone-format" : "";
+ result = result.Replace("[#Phone]", value);
+ }
+
+ // [#Url]
+ if (result.Contains("[#Url]"))
+ {
+ var value = prop.PropertyInfo.GetCustomAttribute() != null ? "url-format" : "";
+ result = result.Replace("[#Url]", value);
+ }
+
+ // [#CreditCard]
+ if (result.Contains("[#CreditCard]"))
+ {
+ var value = prop.PropertyInfo.GetCustomAttribute() != null ? "credit-card-format" : "";
+ result = result.Replace("[#CreditCard]", value);
+ }
+
+ // [#RegularExpression]
+ if (result.Contains("[#RegularExpression]"))
+ {
+ var attr = prop.PropertyInfo.GetCustomAttribute();
+ var value = attr != null ? $"pattern: {attr.Pattern}" : "";
+ result = result.Replace("[#RegularExpression]", value);
+ }
+
+ // [#SmartTypeConstraints] → Type-derived constraints
+ if (result.Contains("[#SmartTypeConstraints]"))
+ {
+ var value = ExtractTypeConstraints(prop.PropertyType);
+ result = result.Replace("[#SmartTypeConstraints]", value);
+ }
+
+ // [#SmartInferenceConstraints] → Smart inference
+ if (result.Contains("[#SmartInferenceConstraints]"))
+ {
+ var value = GetInferredConstraints(prop.PropertyType, prop.Name);
+ result = result.Replace("[#SmartInferenceConstraints]", value);
+ }
+
+ return CleanupPlaceholders(result);
+ }
+
+ ///
+ /// Resolve [#...] placeholders in examples string.
+ ///
+ private static string ResolveExamplesPlaceholders(
+ string template,
+ ToonPropertyAccessor prop)
+ {
+ var result = template;
+
+ // [#GeneratedExample] → Generate example based on type
+ if (result.Contains("[#GeneratedExample]"))
+ {
+ var value = GenerateExampleValue(prop.PropertyType);
+ result = result.Replace("[#GeneratedExample]", value);
+ }
+
+ return CleanupPlaceholders(result);
+ }
+
+ ///
+ /// Clean up empty placeholders and formatting artifacts.
+ ///
+ private static string CleanupPlaceholders(string text)
+ {
+ // Remove unresolved placeholders
+ text = Regex.Replace(text, @"\[#\w+\]", "");
+
+ // Remove double commas
+ text = Regex.Replace(text, @",\s*,", ",");
+
+ // Remove leading/trailing commas
+ text = Regex.Replace(text, @"^,\s*", "");
+ text = Regex.Replace(text, @",\s*$", "");
+
+ // Remove multiple spaces
+ text = Regex.Replace(text, @"\s{2,}", " ");
+
+ return text.Trim();
+ }
+
+ ///
+ /// Generate example value for a type.
+ ///
+ private static string GenerateExampleValue(Type type)
+ {
+ var typeCode = Type.GetTypeCode(type);
+ return typeCode switch
+ {
+ TypeCode.Int32 => "42",
+ TypeCode.Int64 => "1000",
+ TypeCode.String => "example",
+ TypeCode.Boolean => "true",
+ TypeCode.DateTime => DateTime.Now.ToString("yyyy-MM-dd"),
+ TypeCode.Decimal => "99.99",
+ TypeCode.Double => "3.14",
+ TypeCode.Single => "2.71",
+ TypeCode.Byte => "255",
+ _ => "value"
+ };
+ }
+
+ ///
+ /// Get inferred constraints based on property name patterns.
+ ///
+ private static string GetInferredConstraints(Type propertyType, string propertyName)
+ {
+ var constraints = new List();
+ var baseType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
+
+ // Email
+ if (baseType == typeof(string) && propertyName.Contains("Email", StringComparison.OrdinalIgnoreCase))
+ constraints.Add("email-format");
+
+ // URL
+ if (baseType == typeof(string) && propertyName.Contains("Url", StringComparison.OrdinalIgnoreCase))
+ constraints.Add("url-format");
+
+ // Age
+ if (IsIntegerType(baseType) && propertyName.Contains("Age", StringComparison.OrdinalIgnoreCase))
+ constraints.Add("range: 0-150");
+
+ // Count (non-negative)
+ if (IsIntegerType(baseType) && propertyName.EndsWith("Count", StringComparison.OrdinalIgnoreCase))
+ constraints.Add("non-negative");
+
+ return string.Join(", ", constraints);
+ }
+}
diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.DataSection.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.DataSection.cs
new file mode 100644
index 0000000..69bbf51
--- /dev/null
+++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.DataSection.cs
@@ -0,0 +1,482 @@
+using System;
+using System.Collections;
+using System.Globalization;
+using System.Runtime.CompilerServices;
+using System.Text;
+using static AyCode.Core.Helpers.JsonUtilities;
+
+namespace AyCode.Core.Serializers.Toons;
+
+public static partial class AcToonSerializer
+{
+ ///
+ /// Write data section only (for DataOnly mode).
+ ///
+ private static void WriteDataSectionOnly(object value, Type type, ToonSerializationContext context)
+ {
+ WriteDataSection(value, type, context);
+ }
+
+ ///
+ /// Write @data section.
+ ///
+ private static void WriteDataSection(object value, Type type, ToonSerializationContext context)
+ {
+ context.WriteLine("@data {");
+ context.CurrentIndentLevel++;
+
+ WriteValue(value, type, context, 0);
+
+ context.CurrentIndentLevel--;
+ context.WriteLine("}");
+ }
+
+ ///
+ /// Write a value (dispatcher for different types).
+ ///
+ private static void WriteValue(object? value, Type type, ToonSerializationContext context, int depth)
+ {
+ if (value == null)
+ {
+ context.Write("null");
+ return;
+ }
+
+ // Try primitive first
+ if (TryWritePrimitive(value, type, context))
+ return;
+
+ if (depth > context.MaxDepth)
+ {
+ context.Write("null");
+ return;
+ }
+
+ // Check for reference
+ if (context.UseReferenceHandling && context.TryGetExistingRef(value, out var refId))
+ {
+ context.Write($"@ref:{refId}");
+ return;
+ }
+
+ // Handle dictionaries
+ if (value is IDictionary dictionary)
+ {
+ WriteDictionary(dictionary, context, depth);
+ return;
+ }
+
+ // Handle collections/arrays
+ if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
+ {
+ WriteArray(enumerable, type, context, depth);
+ return;
+ }
+
+ // Handle complex objects
+ WriteObject(value, type, context, depth);
+ }
+
+ ///
+ /// Write primitive value inline.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool TryWritePrimitive(object value, Type type, ToonSerializationContext context)
+ {
+ var underlyingType = Nullable.GetUnderlyingType(type) ?? type;
+ var typeCode = Type.GetTypeCode(underlyingType);
+
+ string valueStr;
+ string? typeHint = null;
+
+ switch (typeCode)
+ {
+ case TypeCode.String:
+ var strValue = (string)value;
+ // Check if multi-line string format should be used
+ if (context.Options.UseMultiLineStrings &&
+ strValue.Length > context.Options.MultiLineStringThreshold)
+ {
+ context.Write(FormatMultiLineString(strValue, context));
+ return true;
+ }
+ valueStr = EscapeString(strValue);
+ typeHint = "string";
+ break;
+ case TypeCode.Int32:
+ valueStr = ((int)value).ToString(CultureInfo.InvariantCulture);
+ typeHint = "int32";
+ break;
+ case TypeCode.Int64:
+ valueStr = ((long)value).ToString(CultureInfo.InvariantCulture);
+ typeHint = "int64";
+ break;
+ case TypeCode.Boolean:
+ valueStr = (bool)value ? "true" : "false";
+ typeHint = "bool";
+ break;
+ case TypeCode.Double:
+ var d = (double)value;
+ if (double.IsNaN(d) || double.IsInfinity(d))
+ {
+ valueStr = "null";
+ }
+ else
+ {
+ valueStr = d.ToString("G17", CultureInfo.InvariantCulture);
+ typeHint = "float64";
+ }
+ break;
+ case TypeCode.Decimal:
+ valueStr = ((decimal)value).ToString(CultureInfo.InvariantCulture);
+ typeHint = "decimal";
+ break;
+ case TypeCode.Single:
+ var f = (float)value;
+ if (float.IsNaN(f) || float.IsInfinity(f))
+ {
+ valueStr = "null";
+ }
+ else
+ {
+ valueStr = f.ToString("G9", CultureInfo.InvariantCulture);
+ typeHint = "float32";
+ }
+ break;
+ case TypeCode.DateTime:
+ valueStr = $"\"{((DateTime)value).ToString("O", CultureInfo.InvariantCulture)}\"";
+ typeHint = "datetime";
+ break;
+ case TypeCode.Byte:
+ valueStr = ((byte)value).ToString(CultureInfo.InvariantCulture);
+ typeHint = "byte";
+ break;
+ case TypeCode.Int16:
+ valueStr = ((short)value).ToString(CultureInfo.InvariantCulture);
+ typeHint = "int16";
+ break;
+ case TypeCode.UInt16:
+ valueStr = ((ushort)value).ToString(CultureInfo.InvariantCulture);
+ typeHint = "uint16";
+ break;
+ case TypeCode.UInt32:
+ valueStr = ((uint)value).ToString(CultureInfo.InvariantCulture);
+ typeHint = "uint32";
+ break;
+ case TypeCode.UInt64:
+ valueStr = ((ulong)value).ToString(CultureInfo.InvariantCulture);
+ typeHint = "uint64";
+ break;
+ case TypeCode.SByte:
+ valueStr = ((sbyte)value).ToString(CultureInfo.InvariantCulture);
+ typeHint = "sbyte";
+ break;
+ case TypeCode.Char:
+ valueStr = EscapeString(value.ToString()!);
+ typeHint = "char";
+ break;
+ default:
+ // Check special types
+ if (ReferenceEquals(underlyingType, GuidType))
+ {
+ valueStr = $"\"{((Guid)value).ToString("D")}\"";
+ typeHint = "guid";
+ }
+ else if (ReferenceEquals(underlyingType, DateTimeOffsetType))
+ {
+ valueStr = $"\"{((DateTimeOffset)value).ToString("O", CultureInfo.InvariantCulture)}\"";
+ typeHint = "datetimeoffset";
+ }
+ else if (ReferenceEquals(underlyingType, TimeSpanType))
+ {
+ valueStr = $"\"{((TimeSpan)value).ToString("c", CultureInfo.InvariantCulture)}\"";
+ typeHint = "timespan";
+ }
+ else if (underlyingType.IsEnum)
+ {
+ // Serialize enum as numeric value (not name) for token efficiency
+ // The @types section contains the mapping
+ var enumUnderlyingType = Enum.GetUnderlyingType(underlyingType);
+ var numericValue = Convert.ChangeType(value, enumUnderlyingType);
+ valueStr = Convert.ToString(numericValue, CultureInfo.InvariantCulture)!;
+ typeHint = "enum";
+ }
+ else
+ {
+ return false;
+ }
+ break;
+ }
+
+ context.Write(valueStr);
+
+ // Add inline type hint if enabled
+ if (context.Options.UseInlineTypeHints && typeHint != null)
+ {
+ context.Write($" <{typeHint}>");
+ }
+
+ return true;
+ }
+
+ ///
+ /// Write complex object.
+ ///
+ private static void WriteObject(object value, Type type, ToonSerializationContext context, int depth)
+ {
+ var metadata = GetTypeMetadata(type);
+
+ // Write reference ID if this is a multi-referenced object
+ if (context.UseReferenceHandling && context.ShouldWriteRef(value, out var refId))
+ {
+ context.Write($"@{refId} ");
+ context.MarkAsWritten(value, refId);
+ }
+
+ // Write type name if enabled
+ if (context.Options.WriteTypeNames)
+ {
+ context.Write(metadata.ShortTypeName);
+ }
+
+ context.WriteLine(" {");
+ context.CurrentIndentLevel++;
+
+ var nextDepth = depth + 1;
+
+ // Write properties
+ foreach (var prop in metadata.Properties)
+ {
+ var propValue = prop.GetValue(value);
+
+ // Skip null/default values if option is set
+ if (context.Options.OmitDefaultValues && prop.IsDefaultValue(propValue))
+ continue;
+
+ context.WriteIndent();
+ context.Write(prop.Name);
+
+ // Token optimization: no spaces around '=' when indentation is disabled
+ if (context.Options.UseIndentation)
+ {
+ context.Write(" = ");
+ }
+ else
+ {
+ context.Write('=');
+ }
+
+ if (propValue == null)
+ {
+ context.WriteLine("null");
+ }
+ else
+ {
+ WriteValue(propValue, prop.PropertyType, context, nextDepth);
+ context.WriteLine();
+ }
+ }
+
+ context.CurrentIndentLevel--;
+ context.WriteIndent();
+ context.Write("}");
+ }
+
+ ///
+ /// Write array/collection.
+ ///
+ private static void WriteArray(IEnumerable enumerable, Type type, ToonSerializationContext context, int depth)
+ {
+ var elementType = GetCollectionElementType(type) ?? typeof(object);
+
+ // Get count if possible
+ var count = 0;
+ if (enumerable is ICollection collection)
+ {
+ count = collection.Count;
+ }
+ else
+ {
+ // Fallback: enumerate to count
+ foreach (var _ in enumerable) count++;
+ }
+
+ // Write array header with optional type hint and count
+ if (context.Options.ShowCollectionCount)
+ {
+ var elementTypeName = GetTypeDisplayName(elementType);
+ context.Write($"<{elementTypeName}[]> (count: {count}) ");
+ }
+
+ context.WriteLine("[");
+ context.CurrentIndentLevel++;
+
+ var nextDepth = depth + 1;
+
+ foreach (var item in enumerable)
+ {
+ context.WriteIndent();
+ WriteValue(item, item?.GetType() ?? elementType, context, nextDepth);
+ context.WriteLine();
+ }
+
+ context.CurrentIndentLevel--;
+ context.WriteIndent();
+ context.Write("]");
+ }
+
+ ///
+ /// Get type display name for a type (used in array hints).
+ ///
+ private static string GetTypeDisplayName(Type type)
+ {
+ var typeCode = Type.GetTypeCode(type);
+ return typeCode switch
+ {
+ TypeCode.Int32 => "int32",
+ TypeCode.Int64 => "int64",
+ TypeCode.Double => "float64",
+ TypeCode.Single => "float32",
+ TypeCode.Decimal => "decimal",
+ TypeCode.Boolean => "bool",
+ TypeCode.String => "string",
+ TypeCode.DateTime => "datetime",
+ TypeCode.Byte => "byte",
+ TypeCode.Int16 => "int16",
+ TypeCode.UInt16 => "uint16",
+ TypeCode.UInt32 => "uint32",
+ TypeCode.UInt64 => "uint64",
+ TypeCode.SByte => "sbyte",
+ TypeCode.Char => "char",
+ _ => type.Name.ToLowerInvariant()
+ };
+ }
+
+ ///
+ /// Format multi-line string with triple-quote syntax.
+ ///
+ private static string FormatMultiLineString(string value, ToonSerializationContext context)
+ {
+ var sb = new StringBuilder();
+ sb.Append("\"\"\"");
+
+ if (context.Options.UseIndentation)
+ {
+ sb.AppendLine();
+
+ // Split into lines and indent each
+ var lines = value.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
+ foreach (var line in lines)
+ {
+ for (var i = 0; i <= context.CurrentIndentLevel; i++)
+ {
+ sb.Append(context.Options.IndentString);
+ }
+ sb.AppendLine(line);
+ }
+
+ // Closing quotes with proper indentation
+ for (var i = 0; i < context.CurrentIndentLevel; i++)
+ {
+ sb.Append(context.Options.IndentString);
+ }
+ sb.Append("\"\"\"");
+ }
+ else
+ {
+ sb.Append(value);
+ sb.Append("\"\"\"");
+ }
+
+ return sb.ToString();
+ }
+
+ ///
+ /// Write dictionary.
+ ///
+ private static void WriteDictionary(IDictionary dictionary, ToonSerializationContext context, int depth)
+ {
+ // Write dictionary header with count and type information
+ if (context.Options.ShowCollectionCount)
+ {
+ var dictType = dictionary.GetType();
+ var typeDisplayName = GetDictionaryTypeDisplayName(dictType);
+ context.Write($"<{typeDisplayName}> (count: {dictionary.Count}) ");
+ }
+
+ context.WriteLine("{");
+ context.CurrentIndentLevel++;
+
+ var nextDepth = depth + 1;
+
+ foreach (DictionaryEntry entry in dictionary)
+ {
+ context.WriteIndent();
+
+ // Write key
+ var keyType = entry.Key?.GetType() ?? typeof(object);
+ WriteValue(entry.Key, keyType, context, nextDepth);
+
+ context.Write(" => ");
+
+ // Write value
+ var valueType = entry.Value?.GetType() ?? typeof(object);
+ WriteValue(entry.Value, valueType, context, nextDepth);
+
+ context.WriteLine();
+ }
+
+ context.CurrentIndentLevel--;
+ context.WriteIndent();
+ context.Write("}");
+ }
+
+ ///
+ /// Get type display name for dictionary (e.g., "Dictionary<string, decimal>").
+ ///
+ private static string GetDictionaryTypeDisplayName(Type dictType)
+ {
+ if (IsDictionaryType(dictType, out var keyType, out var valueType))
+ {
+ var keyTypeName = GetSimpleTypeName(keyType);
+ var valueTypeName = GetSimpleTypeName(valueType);
+ return $"Dictionary<{keyTypeName}, {valueTypeName}>";
+ }
+
+ return "dict";
+ }
+
+ ///
+ /// Get simple type name for dictionary type parameters.
+ ///
+ private static string GetSimpleTypeName(Type? type)
+ {
+ if (type == null) return "object";
+
+ var underlying = Nullable.GetUnderlyingType(type);
+ var isNullable = underlying != null;
+ var actualType = underlying ?? type;
+
+ var baseName = Type.GetTypeCode(actualType) switch
+ {
+ TypeCode.Int32 => "int",
+ TypeCode.Int64 => "long",
+ TypeCode.Double => "double",
+ TypeCode.Decimal => "decimal",
+ TypeCode.Single => "float",
+ TypeCode.Boolean => "bool",
+ TypeCode.String => "string",
+ TypeCode.DateTime => "DateTime",
+ TypeCode.Byte => "byte",
+ TypeCode.Int16 => "short",
+ TypeCode.UInt16 => "ushort",
+ TypeCode.UInt32 => "uint",
+ TypeCode.UInt64 => "ulong",
+ TypeCode.SByte => "sbyte",
+ TypeCode.Char => "char",
+ _ => actualType.Name
+ };
+
+ return isNullable ? baseName + "?" : baseName;
+ }
+}
diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.MetaSection.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.MetaSection.cs
new file mode 100644
index 0000000..8ed59d2
--- /dev/null
+++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.MetaSection.cs
@@ -0,0 +1,760 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Reflection;
+using static AyCode.Core.Helpers.JsonUtilities;
+
+namespace AyCode.Core.Serializers.Toons;
+
+public static partial class AcToonSerializer
+{
+ ///
+ /// Write meta section only (for MetaOnly mode).
+ ///
+ private static void WriteMetaSectionOnly(Type type, ToonSerializationContext context)
+ {
+ WriteMetaSection(type, context);
+ }
+
+ ///
+ /// Write @meta and @types sections for a single root type.
+ ///
+ private static void WriteMetaSection(Type type, ToonSerializationContext context)
+ {
+ if (!context.Options.UseMeta) return;
+
+ // Collect all types that need metadata
+ var typesToDocument = new HashSet();
+ CollectTypes(type, typesToDocument);
+
+ WriteMetaSectionCore(typesToDocument, context);
+ }
+
+ ///
+ /// Write @meta and @types sections for a collection of types.
+ ///
+ private static void WriteMetaSection(IEnumerable types, ToonSerializationContext context)
+ {
+ if (!context.Options.UseMeta) return;
+
+ // Collect all types that need metadata (including nested types)
+ var typesToDocument = new HashSet();
+ foreach (var type in types)
+ {
+ CollectTypes(type, typesToDocument);
+ }
+
+ WriteMetaSectionCore(typesToDocument, context);
+ }
+
+ ///
+ /// Core logic for writing @meta and @types sections.
+ ///
+ private static void WriteMetaSectionCore(HashSet typesToDocument, ToonSerializationContext context)
+ {
+ // @meta header
+ context.WriteLine("@meta {");
+ context.CurrentIndentLevel++;
+ context.WriteProperty("version", $"\"{FormatVersion}\"");
+ context.WriteProperty("format", "\"toon\"");
+
+ // Write type list
+ context.WriteIndent();
+ context.Write("types");
+
+ // Token optimization: no spaces around '=' when indentation is disabled
+ if (context.Options.UseIndentation)
+ {
+ context.Write(" = ");
+ }
+ else
+ {
+ context.Write('=');
+ }
+
+ context.Write("[");
+ var first = true;
+ foreach (var t in typesToDocument)
+ {
+ if (!first) context.Write(", ");
+ context.Write($"\"{t.Name}\"");
+ first = false;
+ }
+ context.WriteLine("]");
+
+ context.CurrentIndentLevel--;
+ context.WriteLine("}");
+
+ // Token optimization: skip empty line when indentation is disabled
+ if (context.Options.UseIndentation)
+ {
+ context.WriteLine();
+ }
+
+ // @types section with descriptions
+ context.WriteLine("@types {");
+ context.CurrentIndentLevel++;
+
+ foreach (var t in typesToDocument)
+ {
+ WriteTypeDefinition(t, context);
+ }
+
+ context.CurrentIndentLevel--;
+ context.WriteLine("}");
+ }
+
+ ///
+ /// Collect all types that need documentation (recursive).
+ ///
+ private static void CollectTypes(Type type, HashSet types)
+ {
+ if (IsPrimitiveOrStringFast(type)) return;
+
+ var underlyingType = Nullable.GetUnderlyingType(type) ?? type;
+
+ if (!types.Add(underlyingType)) return; // Already processed
+
+ // Handle collections
+ var elementType = GetCollectionElementType(underlyingType);
+ if (elementType != null)
+ {
+ CollectTypes(elementType, types);
+ return;
+ }
+
+ // Handle dictionaries
+ if (IsDictionaryType(underlyingType, out var keyType, out var valueType))
+ {
+ if (keyType != null) CollectTypes(keyType, types);
+ if (valueType != null) CollectTypes(valueType, types);
+ return;
+ }
+
+ // Handle object properties
+ var metadata = GetTypeMetadata(underlyingType);
+ foreach (var prop in metadata.Properties)
+ {
+ CollectTypes(prop.PropertyType, types);
+ }
+ }
+
+ ///
+ /// Write type definition with property descriptions.
+ ///
+ private static void WriteTypeDefinition(Type type, ToonSerializationContext context)
+ {
+ // Handle enum types specially
+ if (type.IsEnum)
+ {
+ WriteEnumTypeDefinition(type, context);
+ return;
+ }
+
+ var metadata = GetTypeMetadata(type);
+
+ // Type description with fallback chain and placeholder resolution
+ var typeDescription = GetFinalTypeDescription(type, metadata);
+ context.WriteIndentedLine($"{type.Name}: \"{typeDescription}\"");
+
+ // Type-level metadata (enhanced mode only)
+ if (context.Options.UseEnhancedMetadata)
+ {
+ context.CurrentIndentLevel++;
+
+ // Table name
+ var tableName = DetectTableName(type, metadata.CustomDescription);
+ if (!string.IsNullOrEmpty(tableName))
+ {
+ context.WriteIndentedLine($"table-name: \"{tableName}\"");
+ }
+
+ // Purpose
+ var typePurpose = GetFinalTypePurpose(type, metadata);
+ if (!string.IsNullOrEmpty(typePurpose))
+ {
+ context.WriteIndentedLine($"purpose: \"{typePurpose}\"");
+ }
+
+ context.CurrentIndentLevel--;
+ }
+
+ if (metadata.Properties.Length == 0) return;
+
+ // Property descriptions
+ context.CurrentIndentLevel++;
+ foreach (var prop in metadata.Properties)
+ {
+ // Get final values with fallback chain + placeholder resolution
+ var propDescription = GetFinalPropertyDescription(prop, type);
+ var purpose = GetFinalPropertyPurpose(prop, type);
+ var constraints = GetFinalPropertyConstraints(prop, type);
+ var examples = GetFinalPropertyExamples(prop);
+
+ // Detect relationship metadata
+ var relationshipMetadata = DetectRelationshipMetadata(prop.PropertyInfo, prop.CustomDescription);
+
+ var typeHint = prop.TypeDisplayName;
+
+ if (context.Options.UseEnhancedMetadata)
+ {
+ // Enhanced format with constraints and purpose
+ context.WriteIndentedLine($"{prop.Name}: {typeHint}");
+ context.CurrentIndentLevel++;
+ context.WriteIndentedLine($"description: \"{propDescription}\"");
+ if (!string.IsNullOrEmpty(purpose))
+ context.WriteIndentedLine($"purpose: \"{purpose}\"");
+ if (!string.IsNullOrEmpty(constraints))
+ context.WriteIndentedLine($"constraints: \"{constraints}\"");
+ if (!string.IsNullOrEmpty(examples))
+ context.WriteIndentedLine($"examples: \"{examples}\"");
+
+ // Add relationship metadata
+ if (relationshipMetadata.IsPrimaryKey)
+ context.WriteIndentedLine($"primary-key: true");
+ if (!string.IsNullOrEmpty(relationshipMetadata.ForeignKeyNavigationProperty))
+ context.WriteIndentedLine($"foreign-key: \"{relationshipMetadata.ForeignKeyNavigationProperty}\"");
+ if (relationshipMetadata.NavigationType.HasValue)
+ {
+ var navType = relationshipMetadata.NavigationType.Value.ToString();
+ var navTypeFormatted = string.Concat(navType.Select((c, i) => i > 0 && char.IsUpper(c) ? "-" + char.ToLower(c) : char.ToLower(c).ToString()));
+ context.WriteIndentedLine($"navigation: \"{navTypeFormatted}\"");
+ }
+ if (!string.IsNullOrEmpty(relationshipMetadata.InverseProperty))
+ context.WriteIndentedLine($"inverse-property: \"{relationshipMetadata.InverseProperty}\"");
+
+ context.CurrentIndentLevel--;
+ }
+ else
+ {
+ // Simple format
+ var relationshipHint = GetRelationshipHint(relationshipMetadata);
+ var fullDescription = string.IsNullOrEmpty(relationshipHint)
+ ? propDescription
+ : $"{propDescription} ({relationshipHint})";
+ context.WriteIndentedLine($"{prop.Name}: {typeHint} \"{fullDescription}\"");
+ }
+ }
+ context.CurrentIndentLevel--;
+
+ // Token optimization: skip empty line when indentation is disabled
+ if (context.Options.UseIndentation)
+ {
+ context.WriteLine();
+ }
+ }
+
+ ///
+ /// Write enum type definition with values and metadata.
+ ///
+ private static void WriteEnumTypeDefinition(Type enumType, ToonSerializationContext context)
+ {
+ var customDescription = enumType.GetCustomAttribute();
+
+ // Type description with fallback
+ var typeDescription = GetFinalEnumDescription(enumType, customDescription);
+ context.WriteIndentedLine($"{enumType.Name}: enum");
+
+ if (context.Options.UseEnhancedMetadata)
+ {
+ context.CurrentIndentLevel++;
+
+ // Description
+ if (!string.IsNullOrEmpty(typeDescription))
+ {
+ context.WriteIndentedLine($"description: \"{typeDescription}\"");
+ }
+
+ // Purpose
+ var typePurpose = customDescription?.Purpose;
+ if (!string.IsNullOrEmpty(typePurpose))
+ {
+ context.WriteIndentedLine($"purpose: \"{typePurpose}\"");
+ }
+
+ // Underlying type
+ var underlyingType = Enum.GetUnderlyingType(enumType);
+ var underlyingTypeName = Type.GetTypeCode(underlyingType) switch
+ {
+ TypeCode.Byte => "byte",
+ TypeCode.SByte => "sbyte",
+ TypeCode.Int16 => "int16",
+ TypeCode.UInt16 => "uint16",
+ TypeCode.Int32 => "int32",
+ TypeCode.UInt32 => "uint32",
+ TypeCode.Int64 => "int64",
+ TypeCode.UInt64 => "uint64",
+ _ => underlyingType.Name.ToLower()
+ };
+ context.WriteIndentedLine($"underlying-type: \"{underlyingTypeName}\"");
+
+ // Default value (first enum member)
+ var enumValues = Enum.GetValues(enumType);
+ if (enumValues.Length > 0)
+ {
+ var firstValue = enumValues.GetValue(0);
+ var firstValueNumeric = Convert.ChangeType(firstValue, underlyingType);
+ context.WriteIndentedLine($"default-value: {firstValueNumeric}");
+ }
+
+ // Enum members with descriptions
+ context.WriteIndentedLine("values:");
+ context.CurrentIndentLevel++;
+
+ var names = Enum.GetNames(enumType);
+ foreach (var name in names)
+ {
+ var field = enumType.GetField(name);
+ var value = field?.GetValue(null);
+ var numericValue = Convert.ChangeType(value, underlyingType);
+
+ // Get member description from ToonDescription attribute
+ var memberDescription = field?.GetCustomAttribute();
+ var description = memberDescription?.Description;
+
+ // Token optimization: format depends on indentation setting
+ var separator = context.Options.UseIndentation ? " = " : "=";
+
+ if (!string.IsNullOrEmpty(description))
+ {
+ context.WriteIndentedLine($"{name}{separator}{numericValue}");
+ context.CurrentIndentLevel++;
+ context.WriteIndentedLine($"description: \"{description}\"");
+ context.CurrentIndentLevel--;
+ }
+ else
+ {
+ context.WriteIndentedLine($"{name}{separator}{numericValue}");
+ }
+ }
+
+ context.CurrentIndentLevel--;
+ context.CurrentIndentLevel--;
+ }
+ else
+ {
+ // Simple format - just show values
+ var names = Enum.GetNames(enumType);
+ var underlyingType = Enum.GetUnderlyingType(enumType);
+ context.CurrentIndentLevel++;
+
+ // Token optimization: format depends on indentation setting
+ var separator = context.Options.UseIndentation ? " = " : "=";
+
+ foreach (var name in names)
+ {
+ var field = enumType.GetField(name);
+ var value = field?.GetValue(null);
+ var numericValue = Convert.ChangeType(value, underlyingType);
+ context.WriteIndentedLine($"{name}{separator}{numericValue}");
+ }
+
+ context.CurrentIndentLevel--;
+ }
+
+ // Token optimization: skip empty line when indentation is disabled
+ if (context.Options.UseIndentation)
+ {
+ context.WriteLine();
+ }
+ }
+
+ ///
+ /// Get final enum description with fallback chain.
+ /// Priority: ToonDescription.Description -> Microsoft [Description] -> Smart inference
+ ///
+ private static string GetFinalEnumDescription(Type enumType, ToonDescriptionAttribute? customDescription)
+ {
+ // 1. ToonDescription.Description
+ if (!string.IsNullOrWhiteSpace(customDescription?.Description))
+ {
+ return customDescription.Description;
+ }
+
+ // 2. Microsoft [Description] attribute
+ var msDesc = enumType.GetCustomAttribute();
+ if (msDesc != null && !string.IsNullOrWhiteSpace(msDesc.Description))
+ {
+ return msDesc.Description;
+ }
+
+ // 3. Smart inference
+ return $"Enum type with values: {string.Join(", ", Enum.GetNames(enumType))}";
+ }
+
+ ///
+ /// Get description for a type (can be extended with XML comments or attributes).
+ ///
+ private static string GetTypeDescription(Type type)
+ {
+ // For now, generate simple descriptions
+ // TODO: In the future, this could read XML documentation comments
+
+ if (type.IsEnum)
+ return $"Enum type with values: {string.Join(", ", Enum.GetNames(type))}";
+
+ var metadata = GetTypeMetadata(type);
+ if (metadata.IsCollection)
+ return $"Collection of {metadata.ElementType?.Name ?? "items"}";
+ if (metadata.IsDictionary)
+ return "Dictionary mapping keys to values";
+
+ return $"Object of type {type.Name}";
+ }
+
+ ///
+ /// Get description for a property (can be extended with XML comments or attributes).
+ ///
+ private static string GetPropertyDescription(Type declaringType, string propertyName, Type propertyType)
+ {
+ // Enhanced description based on common patterns
+ var isNullable = Nullable.GetUnderlyingType(propertyType) != null || !propertyType.IsValueType;
+ var baseType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
+
+ // Common property name patterns
+ if (propertyName.Equals("Id", StringComparison.OrdinalIgnoreCase))
+ return $"Unique identifier for {declaringType.Name}";
+ if (propertyName.Equals("Name", StringComparison.OrdinalIgnoreCase))
+ return $"Name of the {declaringType.Name}";
+ if (propertyName.Contains("Email", StringComparison.OrdinalIgnoreCase))
+ return "Email address";
+ if (propertyName.Contains("Phone", StringComparison.OrdinalIgnoreCase))
+ return "Phone number";
+ if (propertyName.Contains("Address", StringComparison.OrdinalIgnoreCase))
+ return "Physical or mailing address";
+ if (propertyName.Contains("Date", StringComparison.OrdinalIgnoreCase) || baseType == typeof(DateTime))
+ return $"Date/time value for {propertyName}";
+ if (propertyName.StartsWith("Is", StringComparison.OrdinalIgnoreCase) && baseType == typeof(bool))
+ return $"Boolean flag indicating {propertyName.Substring(2)}";
+ if (propertyName.StartsWith("Has", StringComparison.OrdinalIgnoreCase) && baseType == typeof(bool))
+ return $"Boolean flag indicating possession of {propertyName.Substring(3)}";
+ if (propertyName.EndsWith("Count", StringComparison.OrdinalIgnoreCase) && IsIntegerType(baseType))
+ return $"Count of {propertyName.Replace("Count", "")}";
+
+ // Collection types
+ if (GetCollectionElementType(propertyType) != null)
+ return $"Collection of {GetCollectionElementType(propertyType)?.Name ?? "items"} for {declaringType.Name}";
+
+ // Dictionary types
+ if (IsDictionaryType(propertyType, out _, out _))
+ return $"Dictionary mapping for {propertyName} in {declaringType.Name}";
+
+ // Default
+ return $"Property {propertyName} of type {baseType.Name}{(isNullable ? " (nullable)" : "")}";
+ }
+
+ ///
+ /// Get property constraints (nullable, required, etc.).
+ ///
+ private static string GetPropertyConstraints(Type propertyType, string propertyName)
+ {
+ var constraints = new List();
+
+ var isNullable = Nullable.GetUnderlyingType(propertyType) != null || !propertyType.IsValueType;
+ if (isNullable)
+ constraints.Add("nullable");
+ else
+ constraints.Add("required");
+
+ var baseType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
+
+ // Type-specific constraints
+ if (baseType == typeof(string))
+ {
+ if (propertyName.Contains("Email", StringComparison.OrdinalIgnoreCase))
+ constraints.Add("email-format");
+ if (propertyName.Contains("Url", StringComparison.OrdinalIgnoreCase))
+ constraints.Add("url-format");
+ }
+
+ if (IsIntegerType(baseType))
+ {
+ if (propertyName.Contains("Age", StringComparison.OrdinalIgnoreCase))
+ constraints.Add("range: 0-150");
+ else if (propertyName.EndsWith("Count", StringComparison.OrdinalIgnoreCase))
+ constraints.Add("non-negative");
+ }
+
+ return constraints.Count > 0 ? string.Join(", ", constraints) : "";
+ }
+
+ ///
+ /// Get property purpose (what it's used for).
+ ///
+ private static string GetPropertyPurpose(Type declaringType, string propertyName)
+ {
+ // Common purposes based on property patterns
+ if (propertyName.Equals("Id", StringComparison.OrdinalIgnoreCase))
+ return "Primary key / unique identification";
+ if (propertyName.Contains("CreatedAt", StringComparison.OrdinalIgnoreCase))
+ return "Timestamp when entity was created";
+ if (propertyName.Contains("UpdatedAt", StringComparison.OrdinalIgnoreCase) ||
+ propertyName.Contains("ModifiedAt", StringComparison.OrdinalIgnoreCase))
+ return "Timestamp of last update";
+ if (propertyName.Contains("DeletedAt", StringComparison.OrdinalIgnoreCase))
+ return "Soft delete timestamp";
+ if (propertyName.StartsWith("Is", StringComparison.OrdinalIgnoreCase))
+ return "Status flag";
+ if (propertyName.Contains("Version", StringComparison.OrdinalIgnoreCase))
+ return "Version tracking / concurrency control";
+
+ return "";
+ }
+
+ ///
+ /// Check if type is an integer type.
+ ///
+ private static bool IsIntegerType(Type type)
+ {
+ var typeCode = Type.GetTypeCode(type);
+ return typeCode is TypeCode.Byte or TypeCode.SByte or TypeCode.Int16 or
+ TypeCode.UInt16 or TypeCode.Int32 or TypeCode.UInt32 or
+ TypeCode.Int64 or TypeCode.UInt64;
+ }
+
+ ///
+ /// Get final property description with fallback chain and placeholder resolution.
+ /// Priority: ToonDescription (with placeholders) > Microsoft [Description] > Smart inference
+ ///
+ private static string GetFinalPropertyDescription(ToonPropertyAccessor prop, Type declaringType)
+ {
+ // 1. ToonDescription.Description (if not empty)
+ var customDesc = prop.CustomDescription?.Description;
+
+ if (!string.IsNullOrWhiteSpace(customDesc))
+ {
+ // Has [#...] placeholder? → Resolve it
+ if (customDesc.Contains("[#"))
+ {
+ return ResolveDescriptionPlaceholders(customDesc, prop, declaringType);
+ }
+ // No placeholder → use as-is
+ return customDesc;
+ }
+
+ // 2. Microsoft [Description] attribute
+ var msDesc = prop.PropertyInfo.GetCustomAttribute();
+ if (msDesc != null && !string.IsNullOrWhiteSpace(msDesc.Description))
+ return msDesc.Description;
+
+ // 3. Smart inference (fallback)
+ return GetPropertyDescription(declaringType, prop.Name, prop.PropertyType);
+ }
+
+ ///
+ /// Get final property purpose with fallback chain and placeholder resolution.
+ /// Priority: ToonDescription.Purpose (with placeholders) > Smart inference
+ ///
+ private static string GetFinalPropertyPurpose(ToonPropertyAccessor prop, Type declaringType)
+ {
+ var customPurpose = prop.CustomDescription?.Purpose;
+
+ if (!string.IsNullOrWhiteSpace(customPurpose))
+ {
+ // Has [#...] placeholder? → Resolve it
+ if (customPurpose.Contains("[#"))
+ {
+ return ResolvePurposePlaceholders(customPurpose, prop, declaringType);
+ }
+ return customPurpose;
+ }
+
+ // Fallback: Smart inference
+ return GetPropertyPurpose(declaringType, prop.Name);
+ }
+
+ ///
+ /// Get final property constraints with fallback chain and placeholder resolution.
+ /// Priority: ToonDescription.Constraints (with placeholders merged) > Microsoft attributes > Type constraints > Smart inference
+ ///
+ private static string GetFinalPropertyConstraints(ToonPropertyAccessor prop, Type declaringType)
+ {
+ var customConstraints = prop.CustomDescription?.Constraints;
+
+ if (!string.IsNullOrWhiteSpace(customConstraints))
+ {
+ // Has [#...] placeholder? → Resolve and merge
+ if (customConstraints.Contains("[#"))
+ {
+ // Resolve placeholders first
+ var resolved = ResolveConstraintPlaceholders(customConstraints, prop);
+
+ // Merge with type/smart constraints if resolved contains content
+ if (!string.IsNullOrWhiteSpace(resolved))
+ {
+ var typeConstraints = ExtractTypeConstraints(prop.PropertyType);
+ var inferredConstraints = GetInferredConstraints(prop.PropertyType, prop.Name);
+
+ return MergeConstraints(typeConstraints, null, inferredConstraints, resolved);
+ }
+ }
+ else
+ {
+ // No placeholder → custom wins (replace mode)
+ return customConstraints;
+ }
+ }
+
+ // No custom constraint → Fallback chain
+ var msConstraints = ExtractDataAnnotationConstraints(prop.PropertyInfo);
+ var typeConstraints2 = ExtractTypeConstraints(prop.PropertyType);
+ var inferredConstraints2 = GetInferredConstraints(prop.PropertyType, prop.Name);
+
+ return MergeConstraints(typeConstraints2, msConstraints, inferredConstraints2, null);
+ }
+
+ ///
+ /// Get final property examples with placeholder resolution.
+ ///
+ private static string GetFinalPropertyExamples(ToonPropertyAccessor prop)
+ {
+ var customExamples = prop.CustomDescription?.Examples;
+
+ if (!string.IsNullOrWhiteSpace(customExamples))
+ {
+ // Has [#...] placeholder? → Resolve it
+ if (customExamples.Contains("[#"))
+ {
+ return ResolveExamplesPlaceholders(customExamples, prop);
+ }
+ return customExamples;
+ }
+
+ // No examples
+ return string.Empty;
+ }
+
+ ///
+ /// Get final type description with fallback chain and placeholder resolution.
+ /// Priority: ToonDescription (with placeholders) > Microsoft [Description] > Smart inference
+ ///
+ private static string GetFinalTypeDescription(Type type, ToonTypeMetadata metadata)
+ {
+ // 1. ToonDescription.Description (if not empty)
+ var customDesc = metadata.CustomDescription?.Description;
+
+ if (!string.IsNullOrWhiteSpace(customDesc))
+ {
+ // Has [#...] placeholder? → Resolve it
+ if (customDesc.Contains("[#"))
+ {
+ return ResolveTypeDescriptionPlaceholders(customDesc, type);
+ }
+ // No placeholder → use as-is
+ return customDesc;
+ }
+
+ // 2. Microsoft [Description] attribute
+ var msDesc = type.GetCustomAttribute();
+ if (msDesc != null && !string.IsNullOrWhiteSpace(msDesc.Description))
+ return msDesc.Description;
+
+ // 3. Smart inference (fallback)
+ return GetTypeDescription(type);
+ }
+
+ ///
+ /// Resolve [#...] placeholders in type description string.
+ ///
+ private static string ResolveTypeDescriptionPlaceholders(string template, Type type)
+ {
+ var result = template;
+
+ // [#Description] → Microsoft [Description]
+ if (result.Contains("[#Description]"))
+ {
+ var msDesc = type.GetCustomAttribute();
+ var value = msDesc?.Description ?? "";
+ result = result.Replace("[#Description]", value);
+ }
+
+ // [#DisplayName] → Microsoft [DisplayName]
+ if (result.Contains("[#DisplayName]"))
+ {
+ var displayName = type.GetCustomAttribute();
+ var value = displayName?.DisplayName ?? type.Name;
+ result = result.Replace("[#DisplayName]", value);
+ }
+
+ // [#SmartDescription] → Smart inference
+ if (result.Contains("[#SmartDescription]"))
+ {
+ var value = GetTypeDescription(type);
+ result = result.Replace("[#SmartDescription]", value);
+ }
+
+ return CleanupPlaceholders(result);
+ }
+
+ ///
+ /// Get final type purpose with fallback chain and placeholder resolution.
+ /// Priority: ToonDescription.Purpose (with placeholders) > Smart inference (empty for classes)
+ ///
+ private static string GetFinalTypePurpose(Type type, ToonTypeMetadata metadata)
+ {
+ var customPurpose = metadata.CustomDescription?.Purpose;
+
+ if (!string.IsNullOrWhiteSpace(customPurpose))
+ {
+ // Has [#...] placeholder? → Resolve it
+ if (customPurpose.Contains("[#"))
+ {
+ return ResolveTypePurposePlaceholders(customPurpose, type);
+ }
+ return customPurpose;
+ }
+
+ // No smart inference for class-level purpose - return empty
+ return string.Empty;
+ }
+
+ ///
+ /// Resolve [#...] placeholders in type purpose string.
+ ///
+ private static string ResolveTypePurposePlaceholders(string template, Type type)
+ {
+ var result = template;
+
+ // [#SmartPurpose] → Would be empty for classes, so just remove it
+ if (result.Contains("[#SmartPurpose]"))
+ {
+ result = result.Replace("[#SmartPurpose]", "");
+ }
+
+ return CleanupPlaceholders(result);
+ }
+
+ ///
+ /// Get relationship hint string for simple format output.
+ ///
+ private static string GetRelationshipHint(RelationshipMetadata metadata)
+ {
+ var hints = new List();
+
+ if (metadata.IsPrimaryKey)
+ hints.Add("primary-key");
+
+ if (!string.IsNullOrEmpty(metadata.ForeignKeyNavigationProperty))
+ hints.Add($"fk->{metadata.ForeignKeyNavigationProperty}");
+
+ if (metadata.NavigationType.HasValue)
+ {
+ var navType = metadata.NavigationType.Value switch
+ {
+ ToonRelationType.ManyToOne => "many-to-one",
+ ToonRelationType.OneToMany => "one-to-many",
+ ToonRelationType.OneToOne => "one-to-one",
+ ToonRelationType.ManyToMany => "many-to-many",
+ _ => null
+ };
+ if (navType != null)
+ hints.Add(navType);
+ }
+
+ if (!string.IsNullOrEmpty(metadata.InverseProperty))
+ hints.Add($"inverse:{metadata.InverseProperty}");
+
+ return hints.Count > 0 ? string.Join(", ", hints) : string.Empty;
+ }
+}
diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.RelationshipDetection.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.RelationshipDetection.cs
new file mode 100644
index 0000000..c8f1982
--- /dev/null
+++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.RelationshipDetection.cs
@@ -0,0 +1,435 @@
+using System.Collections;
+using System.Reflection;
+using static AyCode.Core.Helpers.JsonUtilities;
+
+namespace AyCode.Core.Serializers.Toons;
+
+public static partial class AcToonSerializer
+{
+ ///
+ /// Relationship metadata for a property.
+ ///
+ private sealed class RelationshipMetadata
+ {
+ public bool IsPrimaryKey { get; set; }
+ public string? ForeignKeyNavigationProperty { get; set; }
+ public ToonRelationType? NavigationType { get; set; }
+ public string? InverseProperty { get; set; }
+ }
+
+ ///
+ /// Detects relationship metadata for a property with fallback chain.
+ /// Priority: ToonDescription -> EF Core/Linq2Db attributes -> Convention-based detection.
+ ///
+ private static RelationshipMetadata DetectRelationshipMetadata(PropertyInfo property, ToonDescriptionAttribute? customDescription)
+ {
+ var metadata = new RelationshipMetadata();
+
+ // 1. IsPrimaryKey - Priority: ToonDescription -> EF Core [Key] -> Linq2Db [PrimaryKey] -> Convention
+ if (customDescription?.IsPrimaryKey.HasValue == true)
+ {
+ metadata.IsPrimaryKey = customDescription.IsPrimaryKey.Value;
+ }
+ else if (TryGetEFCoreKey(property) || TryGetLinq2DbPrimaryKey(property))
+ {
+ metadata.IsPrimaryKey = true;
+ }
+ else
+ {
+ metadata.IsPrimaryKey = IsConventionPrimaryKey(property);
+ }
+
+ // 2. ForeignKey - Priority: ToonDescription -> EF Core [ForeignKey] -> Linq2Db [Association] -> Convention
+ if (!string.IsNullOrEmpty(customDescription?.ForeignKey))
+ {
+ metadata.ForeignKeyNavigationProperty = customDescription.ForeignKey;
+ }
+ else
+ {
+ var efCoreFk = TryGetEFCoreForeignKey(property);
+ if (!string.IsNullOrEmpty(efCoreFk))
+ {
+ metadata.ForeignKeyNavigationProperty = efCoreFk;
+ }
+ else
+ {
+ metadata.ForeignKeyNavigationProperty = DetectConventionForeignKey(property);
+ }
+ }
+
+ // 3. Navigation - Priority: ToonDescription -> EF Core [InverseProperty] -> Linq2Db [Association] -> Convention
+ if (customDescription?.Navigation.HasValue == true)
+ {
+ metadata.NavigationType = customDescription.Navigation.Value;
+ }
+ else
+ {
+ var linq2dbNav = TryGetLinq2DbNavigation(property);
+ if (linq2dbNav.HasValue)
+ {
+ metadata.NavigationType = linq2dbNav.Value;
+ }
+ else
+ {
+ metadata.NavigationType = DetectConventionNavigation(property);
+ }
+ }
+
+ // 4. InverseProperty - Priority: ToonDescription -> EF Core [InverseProperty]
+ if (!string.IsNullOrEmpty(customDescription?.InverseProperty))
+ {
+ metadata.InverseProperty = customDescription.InverseProperty;
+ }
+ else
+ {
+ var efCoreInverse = TryGetEFCoreInverseProperty(property);
+ if (!string.IsNullOrEmpty(efCoreInverse))
+ {
+ metadata.InverseProperty = efCoreInverse;
+ }
+ }
+
+ return metadata;
+ }
+
+ ///
+ /// Convention: Property named "Id" or "{TypeName}Id" is a primary key.
+ ///
+ private static bool IsConventionPrimaryKey(PropertyInfo property)
+ {
+ if (property.PropertyType != typeof(int) &&
+ property.PropertyType != typeof(long) &&
+ property.PropertyType != typeof(Guid))
+ {
+ return false;
+ }
+
+ return property.Name.Equals("Id", StringComparison.OrdinalIgnoreCase) ||
+ property.Name.Equals(property.DeclaringType?.Name + "Id", StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Convention: Property named "{TypeName}Id" with a corresponding "{TypeName}" navigation property is a foreign key.
+ /// Type-based validation: checks that navigation property is a complex type (not primitive/string).
+ /// Example: "CompanyId" + "Company" property exists and is complex type -> foreign key to "Company".
+ ///
+ private static string? DetectConventionForeignKey(PropertyInfo property)
+ {
+ // Skip non-value types (we don't check specific FK type, could be int/long/Guid/etc)
+ if (!property.PropertyType.IsValueType)
+ {
+ return null;
+ }
+
+ // Check if property name ends with "Id"
+ if (!property.Name.EndsWith("Id", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ // Extract potential navigation property name
+ var navigationPropertyName = property.Name.Substring(0, property.Name.Length - 2);
+
+ // Check if corresponding navigation property exists
+ var declaringType = property.DeclaringType;
+ if (declaringType == null)
+ {
+ return null;
+ }
+
+ var navigationProperty = declaringType.GetProperty(navigationPropertyName, BindingFlags.Public | BindingFlags.Instance);
+
+ // Type-based validation: navigation property must exist and be a complex type (not primitive/string)
+ if (navigationProperty == null || IsPrimitiveOrStringFast(navigationProperty.PropertyType))
+ {
+ return null;
+ }
+
+ // Additional validation: navigation property type should have a primary key (IsPrimaryKey or Id property)
+ if (!HasPrimaryKeyProperty(navigationProperty.PropertyType))
+ {
+ return null;
+ }
+
+ return navigationPropertyName;
+ }
+
+ ///
+ /// Convention: Detect navigation type based on property type.
+ /// Type-based validation with FK lookup:
+ /// - ICollection<T> or List<T> -> OneToMany (if T has primary key)
+ /// - Complex object type -> ManyToOne (if has corresponding FK property and has primary key)
+ ///
+ private static ToonRelationType? DetectConventionNavigation(PropertyInfo property)
+ {
+ var propertyType = property.PropertyType;
+
+ // Skip primitive types and strings
+ if (IsPrimitiveOrStringFast(propertyType))
+ {
+ return null;
+ }
+
+ // Check for collection types (OneToMany)
+ if (typeof(IEnumerable).IsAssignableFrom(propertyType) && propertyType != typeof(string))
+ {
+ // Extract element type from collection
+ var elementType = GetCollectionElementType(propertyType);
+ if (elementType != null && HasPrimaryKeyProperty(elementType))
+ {
+ // It's a collection of entities -> OneToMany
+ return ToonRelationType.OneToMany;
+ }
+ return null;
+ }
+
+ // Complex object type -> check if it's a navigation property
+ // Type-based validation: must have a primary key
+ if (!HasPrimaryKeyProperty(propertyType))
+ {
+ return null;
+ }
+
+ // Look for a corresponding foreign key property to confirm it's a navigation
+ var declaringType = property.DeclaringType;
+ if (declaringType != null)
+ {
+ var foreignKeyPropertyName = property.Name + "Id";
+ var foreignKeyProperty = declaringType.GetProperty(foreignKeyPropertyName, BindingFlags.Public | BindingFlags.Instance);
+
+ // Type-based validation: FK must be a value type
+ if (foreignKeyProperty != null && foreignKeyProperty.PropertyType.IsValueType)
+ {
+ // Found corresponding foreign key -> ManyToOne
+ return ToonRelationType.ManyToOne;
+ }
+ }
+
+ // No corresponding foreign key found
+ // Return null to avoid false positives
+ return null;
+ }
+
+ ///
+ /// Check if a type has a primary key property.
+ /// First checks for IsPrimaryKey=true in ToonDescription, then falls back to "Id" property.
+ ///
+ private static bool HasPrimaryKeyProperty(Type type)
+ {
+ if (IsPrimitiveOrStringFast(type))
+ {
+ return false;
+ }
+
+ var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
+
+ // First: check for ToonDescription with IsPrimaryKey = true
+ foreach (var prop in properties)
+ {
+ var customDescription = prop.GetCustomAttribute();
+ if (customDescription?.IsPrimaryKey == true)
+ {
+ return true;
+ }
+ }
+
+ // Fallback: check for "Id" or "{TypeName}Id" property
+ foreach (var prop in properties)
+ {
+ if (prop.Name.Equals("Id", StringComparison.OrdinalIgnoreCase) ||
+ prop.Name.Equals(type.Name + "Id", StringComparison.OrdinalIgnoreCase))
+ {
+ // Must be a value type (int, long, Guid, etc.)
+ if (prop.PropertyType.IsValueType)
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ #region EF Core Attribute Detection (reflection-based, no dependency)
+
+ ///
+ /// Detect EF Core [Key] attribute via reflection (no EF Core dependency).
+ ///
+ private static bool TryGetEFCoreKey(PropertyInfo property)
+ {
+ var keyAttr = property.GetCustomAttributes()
+ .FirstOrDefault(a => a.GetType().Name == "KeyAttribute");
+ return keyAttr != null;
+ }
+
+ ///
+ /// Detect EF Core [ForeignKey("NavigationPropertyName")] attribute via reflection.
+ ///
+ private static string? TryGetEFCoreForeignKey(PropertyInfo property)
+ {
+ var fkAttr = property.GetCustomAttributes()
+ .FirstOrDefault(a => a.GetType().Name == "ForeignKeyAttribute");
+
+ if (fkAttr != null)
+ {
+ // Get the Name property value (navigation property name)
+ var nameProp = fkAttr.GetType().GetProperty("Name");
+ return nameProp?.GetValue(fkAttr) as string;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Detect EF Core [InverseProperty("PropertyName")] attribute via reflection.
+ ///
+ private static string? TryGetEFCoreInverseProperty(PropertyInfo property)
+ {
+ var inversePropAttr = property.GetCustomAttributes()
+ .FirstOrDefault(a => a.GetType().Name == "InversePropertyAttribute");
+
+ if (inversePropAttr != null)
+ {
+ var propertyProp = inversePropAttr.GetType().GetProperty("Property");
+ return propertyProp?.GetValue(inversePropAttr) as string;
+ }
+
+ return null;
+ }
+
+ #endregion
+
+ #region Linq2Db Attribute Detection (reflection-based, no dependency)
+
+ ///
+ /// Detect Linq2Db [PrimaryKey] attribute via reflection (no Linq2Db dependency).
+ ///
+ private static bool TryGetLinq2DbPrimaryKey(PropertyInfo property)
+ {
+ var pkAttr = property.GetCustomAttributes()
+ .FirstOrDefault(a => a.GetType().Name == "PrimaryKeyAttribute");
+ return pkAttr != null;
+ }
+
+ ///
+ /// Detect Linq2Db [Association] attribute and determine navigation type.
+ /// Supports simple ThisKey/OtherKey associations. Expression-based associations are skipped.
+ ///
+ private static ToonRelationType? TryGetLinq2DbNavigation(PropertyInfo property)
+ {
+ var assocAttr = property.GetCustomAttributes()
+ .FirstOrDefault(a => a.GetType().Name == "AssociationAttribute");
+
+ if (assocAttr == null)
+ return null;
+
+ // Check if it's expression-based (has QueryExpressionMethod property set)
+ var queryExprMethod = assocAttr.GetType().GetProperty("QueryExpressionMethod")?.GetValue(assocAttr) as string;
+ if (!string.IsNullOrEmpty(queryExprMethod))
+ {
+ // Expression-based many-to-many - too complex, skip and fallback to convention
+ return null;
+ }
+
+ // Simple association with ThisKey/OtherKey
+ var thisKey = assocAttr.GetType().GetProperty("ThisKey")?.GetValue(assocAttr) as string;
+ var otherKey = assocAttr.GetType().GetProperty("OtherKey")?.GetValue(assocAttr) as string;
+
+ var propertyType = property.PropertyType;
+
+ // Check if it's a collection (OneToMany or ManyToMany)
+ if (typeof(IEnumerable).IsAssignableFrom(propertyType) && propertyType != typeof(string))
+ {
+ // Collection with ThisKey + OtherKey -> could be ManyToMany or OneToMany
+ // If both ThisKey and OtherKey are set -> likely ManyToMany
+ // If only ThisKey -> OneToMany
+ if (!string.IsNullOrEmpty(thisKey) && !string.IsNullOrEmpty(otherKey))
+ {
+ // Could be ManyToMany, but without junction table info, treat as OneToMany
+ return ToonRelationType.OneToMany;
+ }
+ else
+ {
+ return ToonRelationType.OneToMany;
+ }
+ }
+ else
+ {
+ // Single object -> ManyToOne or OneToOne
+ // Typically ManyToOne
+ return ToonRelationType.ManyToOne;
+ }
+ }
+
+ #endregion
+
+ #region TableName Detection (class-level)
+
+ ///
+ /// Detect table name for a type with fallback chain.
+ /// Priority: ToonDescription.TableName -> EF Core [Table] -> Linq2Db [Table] -> Convention (class name).
+ ///
+ internal static string? DetectTableName(Type type, ToonDescriptionAttribute? customDescription)
+ {
+ // 1. ToonDescription.TableName (explicit override)
+ if (!string.IsNullOrEmpty(customDescription?.TableName))
+ {
+ return customDescription.TableName;
+ }
+
+ // 2. EF Core [Table("name")] attribute (primary)
+ var efCoreTable = TryGetEFCoreTableName(type);
+ if (!string.IsNullOrEmpty(efCoreTable))
+ {
+ return efCoreTable;
+ }
+
+ // 3. Linq2Db [Table(Name = "name")] attribute (if EF Core not found)
+ var linq2dbTable = TryGetLinq2DbTableName(type);
+ if (!string.IsNullOrEmpty(linq2dbTable))
+ {
+ return linq2dbTable;
+ }
+
+ // 4. Convention: class name (fallback)
+ return type.Name;
+ }
+
+ ///
+ /// Detect EF Core [Table("name")] or [Table(Name = "name", Schema = "schema")] attribute via reflection.
+ ///
+ private static string? TryGetEFCoreTableName(Type type)
+ {
+ var tableAttr = type.GetCustomAttributes()
+ .FirstOrDefault(a => a.GetType().Name == "TableAttribute");
+
+ if (tableAttr != null)
+ {
+ // EF Core TableAttribute has "Name" property
+ var nameProp = tableAttr.GetType().GetProperty("Name");
+ return nameProp?.GetValue(tableAttr) as string;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Detect Linq2Db [Table(Name = "name")] attribute via reflection.
+ ///
+ private static string? TryGetLinq2DbTableName(Type type)
+ {
+ var tableAttr = type.GetCustomAttributes()
+ .FirstOrDefault(a => a.GetType().Name == "TableAttribute");
+
+ if (tableAttr != null)
+ {
+ // Linq2Db TableAttribute also has "Name" property
+ var nameProp = tableAttr.GetType().GetProperty("Name");
+ return nameProp?.GetValue(tableAttr) as string;
+ }
+
+ return null;
+ }
+
+ #endregion
+}
diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs
new file mode 100644
index 0000000..8dd49b9
--- /dev/null
+++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs
@@ -0,0 +1,245 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+using ReferenceEqualityComparer = AyCode.Core.Serializers.Jsons.ReferenceEqualityComparer;
+
+namespace AyCode.Core.Serializers.Toons;
+
+public static partial class AcToonSerializer
+{
+ #region Context Pool
+
+ private static class ToonSerializationContextPool
+ {
+ private static readonly ConcurrentQueue Pool = new();
+ private const int MaxPoolSize = 16;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ToonSerializationContext Get(AcToonSerializerOptions options)
+ {
+ if (Pool.TryDequeue(out var context))
+ {
+ context.Reset(options);
+ return context;
+ }
+ return new ToonSerializationContext(options);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Return(ToonSerializationContext context)
+ {
+ if (Pool.Count < MaxPoolSize)
+ {
+ context.Clear();
+ Pool.Enqueue(context);
+ }
+ }
+ }
+
+ #endregion
+
+ #region Serialization Context
+
+ ///
+ /// Pooled context for Toon serialization.
+ /// Handles output building, indentation, and reference tracking.
+ ///
+ private sealed class ToonSerializationContext
+ {
+ private readonly StringBuilder _builder;
+ private Dictionary