Enhance Toon serialization with relationship & table metadata
- Add automatic detection of primary/foreign keys, navigation types, and table names via ToonDescription, EF Core/Linq2Db attributes, or convention - Extend ToonDescriptionAttribute with IsPrimaryKey, ForeignKey, Navigation, InverseProperty, and TableName - Output relationship and table metadata in @types section (enhanced and simple modes) - Document enums in @types with numeric values and descriptions - Optimize token usage: compact output when indentation is off - Show dictionary key/value types in output - Add SerializeMetadata(IEnumerable<Type>) API for multi-type docs - Refactor and improve documentation throughout
This commit is contained in:
parent
bbb21dbb67
commit
d2caa2234d
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(dotnet build:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -194,9 +194,11 @@ public static partial class AcToonSerializer
|
|||
}
|
||||
else if (underlyingType.IsEnum)
|
||||
{
|
||||
var enumValue = Convert.ToInt32(value);
|
||||
var enumName = Enum.GetName(underlyingType, value);
|
||||
valueStr = enumName != null ? $"\"{enumName}\"" : enumValue.ToString(CultureInfo.InvariantCulture);
|
||||
// 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
|
||||
|
|
@ -253,7 +255,16 @@ public static partial class AcToonSerializer
|
|||
|
||||
context.WriteIndent();
|
||||
context.Write(prop.Name);
|
||||
context.Write(" = ");
|
||||
|
||||
// Token optimization: no spaces around '=' when indentation is disabled
|
||||
if (context.Options.UseIndentation)
|
||||
{
|
||||
context.Write(" = ");
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Write('=');
|
||||
}
|
||||
|
||||
if (propValue == null)
|
||||
{
|
||||
|
|
@ -385,10 +396,12 @@ public static partial class AcToonSerializer
|
|||
/// </summary>
|
||||
private static void WriteDictionary(IDictionary dictionary, ToonSerializationContext context, int depth)
|
||||
{
|
||||
// Write dictionary header with count
|
||||
// Write dictionary header with count and type information
|
||||
if (context.Options.ShowCollectionCount)
|
||||
{
|
||||
context.Write($"<dict> (count: {dictionary.Count}) ");
|
||||
var dictType = dictionary.GetType();
|
||||
var typeDisplayName = GetDictionaryTypeDisplayName(dictType);
|
||||
context.Write($"<{typeDisplayName}> (count: {dictionary.Count}) ");
|
||||
}
|
||||
|
||||
context.WriteLine("{");
|
||||
|
|
@ -417,4 +430,53 @@ public static partial class AcToonSerializer
|
|||
context.WriteIndent();
|
||||
context.Write("}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get type display name for dictionary (e.g., "Dictionary<string, decimal>").
|
||||
/// </summary>
|
||||
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";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get simple type name for dictionary type parameters.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,25 +18,62 @@ public static partial class AcToonSerializer
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write @meta and @types sections.
|
||||
/// Write @meta and @types sections for a single root type.
|
||||
/// </summary>
|
||||
private static void WriteMetaSection(Type type, ToonSerializationContext context)
|
||||
{
|
||||
if (!context.Options.UseMeta) return;
|
||||
|
||||
// Collect all types that need metadata
|
||||
var typesToDocument = new HashSet<Type>();
|
||||
CollectTypes(type, typesToDocument);
|
||||
|
||||
WriteMetaSectionCore(typesToDocument, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write @meta and @types sections for a collection of types.
|
||||
/// </summary>
|
||||
private static void WriteMetaSection(IEnumerable<Type> types, ToonSerializationContext context)
|
||||
{
|
||||
if (!context.Options.UseMeta) return;
|
||||
|
||||
// Collect all types that need metadata (including nested types)
|
||||
var typesToDocument = new HashSet<Type>();
|
||||
foreach (var type in types)
|
||||
{
|
||||
CollectTypes(type, typesToDocument);
|
||||
}
|
||||
|
||||
WriteMetaSectionCore(typesToDocument, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core logic for writing @meta and @types sections.
|
||||
/// </summary>
|
||||
private static void WriteMetaSectionCore(HashSet<Type> typesToDocument, ToonSerializationContext context)
|
||||
{
|
||||
// @meta header
|
||||
context.WriteLine("@meta {");
|
||||
context.CurrentIndentLevel++;
|
||||
context.WriteProperty("version", $"\"{FormatVersion}\"");
|
||||
context.WriteProperty("format", "\"toon\"");
|
||||
|
||||
// Collect all types that need metadata
|
||||
var typesToDocument = new HashSet<Type>();
|
||||
CollectTypes(type, typesToDocument);
|
||||
|
||||
// Write type list
|
||||
context.WriteIndent();
|
||||
context.Write("types = [");
|
||||
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)
|
||||
{
|
||||
|
|
@ -48,7 +85,12 @@ public static partial class AcToonSerializer
|
|||
|
||||
context.CurrentIndentLevel--;
|
||||
context.WriteLine("}");
|
||||
context.WriteLine();
|
||||
|
||||
// Token optimization: skip empty line when indentation is disabled
|
||||
if (context.Options.UseIndentation)
|
||||
{
|
||||
context.WriteLine();
|
||||
}
|
||||
|
||||
// @types section with descriptions
|
||||
context.WriteLine("@types {");
|
||||
|
|
@ -103,22 +145,39 @@ public static partial class AcToonSerializer
|
|||
/// </summary>
|
||||
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 purpose (if enhanced metadata is enabled)
|
||||
// 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.CurrentIndentLevel++;
|
||||
context.WriteIndentedLine($"purpose: \"{typePurpose}\"");
|
||||
context.CurrentIndentLevel--;
|
||||
}
|
||||
|
||||
context.CurrentIndentLevel--;
|
||||
}
|
||||
|
||||
if (metadata.Properties.Length == 0) return;
|
||||
|
|
@ -133,6 +192,9 @@ public static partial class AcToonSerializer
|
|||
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)
|
||||
|
|
@ -148,16 +210,177 @@ public static partial class AcToonSerializer
|
|||
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
|
||||
context.WriteIndentedLine($"{prop.Name}: {typeHint} \"{propDescription}\"");
|
||||
var relationshipHint = GetRelationshipHint(relationshipMetadata);
|
||||
var fullDescription = string.IsNullOrEmpty(relationshipHint)
|
||||
? propDescription
|
||||
: $"{propDescription} ({relationshipHint})";
|
||||
context.WriteIndentedLine($"{prop.Name}: {typeHint} \"{fullDescription}\"");
|
||||
}
|
||||
}
|
||||
context.CurrentIndentLevel--;
|
||||
context.WriteLine();
|
||||
|
||||
// Token optimization: skip empty line when indentation is disabled
|
||||
if (context.Options.UseIndentation)
|
||||
{
|
||||
context.WriteLine();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write enum type definition with values and metadata.
|
||||
/// </summary>
|
||||
private static void WriteEnumTypeDefinition(Type enumType, ToonSerializationContext context)
|
||||
{
|
||||
var customDescription = enumType.GetCustomAttribute<ToonDescriptionAttribute>();
|
||||
|
||||
// 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<ToonDescriptionAttribute>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get final enum description with fallback chain.
|
||||
/// Priority: ToonDescription.Description -> Microsoft [Description] -> Smart inference
|
||||
/// </summary>
|
||||
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<System.ComponentModel.DescriptionAttribute>();
|
||||
if (msDesc != null && !string.IsNullOrWhiteSpace(msDesc.Description))
|
||||
{
|
||||
return msDesc.Description;
|
||||
}
|
||||
|
||||
// 3. Smart inference
|
||||
return $"Enum type with values: {string.Join(", ", Enum.GetNames(enumType))}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -501,4 +724,37 @@ public static partial class AcToonSerializer
|
|||
|
||||
return CleanupPlaceholders(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get relationship hint string for simple format output.
|
||||
/// </summary>
|
||||
private static string GetRelationshipHint(RelationshipMetadata metadata)
|
||||
{
|
||||
var hints = new List<string>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Relationship metadata for a property.
|
||||
/// </summary>
|
||||
private sealed class RelationshipMetadata
|
||||
{
|
||||
public bool IsPrimaryKey { get; set; }
|
||||
public string? ForeignKeyNavigationProperty { get; set; }
|
||||
public ToonRelationType? NavigationType { get; set; }
|
||||
public string? InverseProperty { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects relationship metadata for a property with fallback chain.
|
||||
/// Priority: ToonDescription -> EF Core/Linq2Db attributes -> Convention-based detection.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convention: Property named "Id" or "{TypeName}Id" is a primary key.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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".
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a type has a primary key property.
|
||||
/// First checks for IsPrimaryKey=true in ToonDescription, then falls back to "Id" property.
|
||||
/// </summary>
|
||||
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<ToonDescriptionAttribute>();
|
||||
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)
|
||||
|
||||
/// <summary>
|
||||
/// Detect EF Core [Key] attribute via reflection (no EF Core dependency).
|
||||
/// </summary>
|
||||
private static bool TryGetEFCoreKey(PropertyInfo property)
|
||||
{
|
||||
var keyAttr = property.GetCustomAttributes()
|
||||
.FirstOrDefault(a => a.GetType().Name == "KeyAttribute");
|
||||
return keyAttr != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detect EF Core [ForeignKey("NavigationPropertyName")] attribute via reflection.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detect EF Core [InverseProperty("PropertyName")] attribute via reflection.
|
||||
/// </summary>
|
||||
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)
|
||||
|
||||
/// <summary>
|
||||
/// Detect Linq2Db [PrimaryKey] attribute via reflection (no Linq2Db dependency).
|
||||
/// </summary>
|
||||
private static bool TryGetLinq2DbPrimaryKey(PropertyInfo property)
|
||||
{
|
||||
var pkAttr = property.GetCustomAttributes()
|
||||
.FirstOrDefault(a => a.GetType().Name == "PrimaryKeyAttribute");
|
||||
return pkAttr != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detect Linq2Db [Association] attribute and determine navigation type.
|
||||
/// Supports simple ThisKey/OtherKey associations. Expression-based associations are skipped.
|
||||
/// </summary>
|
||||
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)
|
||||
|
||||
/// <summary>
|
||||
/// Detect table name for a type with fallback chain.
|
||||
/// Priority: ToonDescription.TableName -> EF Core [Table] -> Linq2Db [Table] -> Convention (class name).
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detect EF Core [Table("name")] or [Table(Name = "name", Schema = "schema")] attribute via reflection.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detect Linq2Db [Table(Name = "name")] attribute via reflection.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
|
|
@ -204,7 +204,17 @@ public static partial class AcToonSerializer
|
|||
{
|
||||
WriteIndent();
|
||||
_builder.Append(name);
|
||||
_builder.Append(" = ");
|
||||
|
||||
// Token optimization: no spaces around '=' when indentation is disabled
|
||||
if (Options.UseIndentation)
|
||||
{
|
||||
_builder.Append(" = ");
|
||||
}
|
||||
else
|
||||
{
|
||||
_builder.Append('=');
|
||||
}
|
||||
|
||||
_builder.Append(value);
|
||||
|
||||
if (inlineComment != null && Options.UseInlineComments)
|
||||
|
|
|
|||
|
|
@ -106,6 +106,46 @@ public static partial class AcToonSerializer
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize metadata only for a collection of types (no data instances needed).
|
||||
/// Useful for documenting multiple types at once, or when you only have Type objects.
|
||||
/// </summary>
|
||||
/// <param name="types">Types to document</param>
|
||||
/// <param name="options">Serialization options (optional, defaults to MetaOnly preset)</param>
|
||||
/// <returns>Metadata-only Toon format string with @meta and @types sections</returns>
|
||||
public static string SerializeMetadata(IEnumerable<Type> types, AcToonSerializerOptions? options = null)
|
||||
{
|
||||
// Return empty string if no types provided
|
||||
var typesList = types?.ToList();
|
||||
if (typesList == null || typesList.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
options ??= AcToonSerializerOptions.MetaOnly;
|
||||
|
||||
var context = ToonSerializationContextPool.Get(options);
|
||||
try
|
||||
{
|
||||
WriteMetaSection(typesList, context);
|
||||
return context.GetResult();
|
||||
}
|
||||
finally
|
||||
{
|
||||
ToonSerializationContextPool.Return(context);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize metadata only for multiple types (params array overload).
|
||||
/// </summary>
|
||||
/// <param name="types">Types to document</param>
|
||||
/// <returns>Metadata-only Toon format string with @meta and @types sections</returns>
|
||||
public static string SerializeMetadata(params Type[] types)
|
||||
{
|
||||
return SerializeMetadata((IEnumerable<Type>)types);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Primitive Serialization
|
||||
|
|
|
|||
|
|
@ -1,5 +1,34 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AyCode.Core.Serializers.Toons;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the type of relationship between entities.
|
||||
/// </summary>
|
||||
public enum ToonRelationType
|
||||
{
|
||||
/// <summary>
|
||||
/// Many-to-one relationship (e.g., Person.Company -> Company).
|
||||
/// </summary>
|
||||
ManyToOne,
|
||||
|
||||
/// <summary>
|
||||
/// One-to-many relationship (e.g., Company.Employees -> Person[]).
|
||||
/// </summary>
|
||||
OneToMany,
|
||||
|
||||
/// <summary>
|
||||
/// One-to-one relationship.
|
||||
/// </summary>
|
||||
OneToOne,
|
||||
|
||||
/// <summary>
|
||||
/// Many-to-many relationship.
|
||||
/// </summary>
|
||||
ManyToMany
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides custom description metadata for Toon serialization with flexible fallback and placeholder support.
|
||||
/// This attribute can be applied to classes and properties to provide rich contextual information
|
||||
|
|
@ -22,6 +51,31 @@ namespace AyCode.Core.Serializers.Toons;
|
|||
/// </summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <para><b>SUPPORTED PLACEHOLDERS:</b></para>
|
||||
/// <list type="table">
|
||||
/// <listheader>
|
||||
/// <term>Placeholder</term>
|
||||
/// <description>Resolves To (Where: C=Class, D=Description, P=Purpose, C=Constraints, E=Examples)</description>
|
||||
/// </listheader>
|
||||
/// <item><term>[#Description]</term><description>Microsoft [Description] attribute (Class.D, Property.D)</description></item>
|
||||
/// <item><term>[#DisplayName]</term><description>Microsoft [DisplayName] attribute (Class.D, Property.D)</description></item>
|
||||
/// <item><term>[#SmartDescription]</term><description>Auto-inferred description (Class.D, Property.D)</description></item>
|
||||
/// <item><term>[#SmartPurpose]</term><description>Auto-inferred purpose (Property.P only, empty for classes)</description></item>
|
||||
/// <item><term>[#Range]</term><description>Microsoft [Range] attribute → "range: min-max" (Property.C)</description></item>
|
||||
/// <item><term>[#Required]</term><description>Microsoft [Required] attribute → "required" (Property.C)</description></item>
|
||||
/// <item><term>[#MaxLength]</term><description>Microsoft [MaxLength] attribute → "max-length: N" (Property.C)</description></item>
|
||||
/// <item><term>[#MinLength]</term><description>Microsoft [MinLength] attribute → "min-length: N" (Property.C)</description></item>
|
||||
/// <item><term>[#StringLength]</term><description>Microsoft [StringLength] attribute → "length: min-max" (Property.C)</description></item>
|
||||
/// <item><term>[#EmailAddress]</term><description>Microsoft [EmailAddress] attribute → "email-format" (Property.C)</description></item>
|
||||
/// <item><term>[#Phone]</term><description>Microsoft [Phone] attribute → "phone-format" (Property.C)</description></item>
|
||||
/// <item><term>[#Url]</term><description>Microsoft [Url] attribute → "url-format" (Property.C)</description></item>
|
||||
/// <item><term>[#CreditCard]</term><description>Microsoft [CreditCard] attribute → "credit-card-format" (Property.C)</description></item>
|
||||
/// <item><term>[#RegularExpression]</term><description>Microsoft [RegularExpression] → "pattern: ..." (Property.C)</description></item>
|
||||
/// <item><term>[#SmartTypeConstraints]</term><description>Type-derived constraints (nullable, numeric, etc.) (Property.C)</description></item>
|
||||
/// <item><term>[#SmartInferenceConstraints]</term><description>Auto-inferred constraints (email-format, range, etc.) (Property.C)</description></item>
|
||||
/// <item><term>[#SmartGeneratedExample]</term><description>Auto-generated example value (Property.E)</description></item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para><b>USAGE MODES:</b></para>
|
||||
///
|
||||
/// <para><b>1. FULL CUSTOM (all properties specified):</b></para>
|
||||
|
|
@ -107,31 +161,6 @@ namespace AyCode.Core.Serializers.Toons;
|
|||
/// // Result: "Object of type GuestUser" (smart inference)
|
||||
/// </code>
|
||||
///
|
||||
/// <para><b>SUPPORTED PLACEHOLDERS:</b></para>
|
||||
/// <list type="table">
|
||||
/// <listheader>
|
||||
/// <term>Placeholder</term>
|
||||
/// <description>Resolves To (Where: C=Class, D=Description, P=Purpose, C=Constraints, E=Examples)</description>
|
||||
/// </listheader>
|
||||
/// <item><term>[#Description]</term><description>Microsoft [Description] attribute (Class.D, Property.D)</description></item>
|
||||
/// <item><term>[#DisplayName]</term><description>Microsoft [DisplayName] attribute (Class.D, Property.D)</description></item>
|
||||
/// <item><term>[#SmartDescription]</term><description>Auto-inferred description (Class.D, Property.D)</description></item>
|
||||
/// <item><term>[#SmartPurpose]</term><description>Auto-inferred purpose (Property.P only, empty for classes)</description></item>
|
||||
/// <item><term>[#Range]</term><description>Microsoft [Range] attribute → "range: min-max" (Property.C)</description></item>
|
||||
/// <item><term>[#Required]</term><description>Microsoft [Required] attribute → "required" (Property.C)</description></item>
|
||||
/// <item><term>[#MaxLength]</term><description>Microsoft [MaxLength] attribute → "max-length: N" (Property.C)</description></item>
|
||||
/// <item><term>[#MinLength]</term><description>Microsoft [MinLength] attribute → "min-length: N" (Property.C)</description></item>
|
||||
/// <item><term>[#StringLength]</term><description>Microsoft [StringLength] attribute → "length: min-max" (Property.C)</description></item>
|
||||
/// <item><term>[#EmailAddress]</term><description>Microsoft [EmailAddress] attribute → "email-format" (Property.C)</description></item>
|
||||
/// <item><term>[#Phone]</term><description>Microsoft [Phone] attribute → "phone-format" (Property.C)</description></item>
|
||||
/// <item><term>[#Url]</term><description>Microsoft [Url] attribute → "url-format" (Property.C)</description></item>
|
||||
/// <item><term>[#CreditCard]</term><description>Microsoft [CreditCard] attribute → "credit-card-format" (Property.C)</description></item>
|
||||
/// <item><term>[#RegularExpression]</term><description>Microsoft [RegularExpression] → "pattern: ..." (Property.C)</description></item>
|
||||
/// <item><term>[#SmartTypeConstraints]</term><description>Type-derived constraints (nullable, numeric, etc.) (Property.C)</description></item>
|
||||
/// <item><term>[#SmartInferenceConstraints]</term><description>Auto-inferred constraints (email-format, range, etc.) (Property.C)</description></item>
|
||||
/// <item><term>[#SmartGeneratedExample]</term><description>Auto-generated example value (Property.E)</description></item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para><b>BEST PRACTICES:</b></para>
|
||||
/// <list type="bullet">
|
||||
/// <item>Use placeholders ([#...]) when you want to MERGE with existing Microsoft attributes</item>
|
||||
|
|
@ -213,6 +242,38 @@ public sealed class ToonDescriptionAttribute : Attribute
|
|||
/// </summary>
|
||||
public string? Examples { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether this property is a primary key.
|
||||
/// If not explicitly set, convention-based detection will be used (e.g., property named "Id").
|
||||
/// </summary>
|
||||
public bool? IsPrimaryKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the foreign key navigation property name.
|
||||
/// If not explicitly set, convention-based detection will be used (e.g., "CompanyId" -> "Company").
|
||||
/// Example: For a property "CompanyId", set ForeignKey = "Company" to indicate it references the Company navigation property.
|
||||
/// </summary>
|
||||
public string? ForeignKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the relationship type for navigation properties.
|
||||
/// If not explicitly set, convention-based detection will be used based on property type.
|
||||
/// </summary>
|
||||
public ToonRelationType? Navigation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the inverse navigation property name for bidirectional relationships.
|
||||
/// Example: For Company.Employees, set InverseProperty = "Company" to indicate the inverse property on Person.
|
||||
/// </summary>
|
||||
public string? InverseProperty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the database table name for this entity (class-level only).
|
||||
/// If not explicitly set, will fallback to EF Core [Table] or Linq2Db [Table] attributes, then to class name.
|
||||
/// Example: TableName = "tbl_Persons" for custom table naming.
|
||||
/// </summary>
|
||||
public string? TableName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the ToonDescriptionAttribute with the specified description.
|
||||
/// </summary>
|
||||
|
|
|
|||
Loading…
Reference in New Issue