diff --git a/.editorconfig b/.editorconfig index fd356df..eb42d03 100644 --- a/.editorconfig +++ b/.editorconfig @@ -80,8 +80,6 @@ dotnet_analyzer_diagnostic.severity = none # .NET Style Rules # https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/language-rules#net-style-rules [*.{cs,csx,cake,vb,vbx}] -indent_style = tab - # "this." and "Me." qualifiers dotnet_style_qualification_for_field = false:none dotnet_style_qualification_for_property = false:none diff --git a/ser/CodeGen.cs b/lib/CodeGen.cs similarity index 93% rename from ser/CodeGen.cs rename to lib/CodeGen.cs index 06f31bf..5931b48 100644 --- a/ser/CodeGen.cs +++ b/lib/CodeGen.cs @@ -24,7 +24,7 @@ public record CodeGenConfig : imm.Recorded public ImmutableDictionary> WLFields { get; init; } = ImmutableDictionary>.Empty; // Default member types to process - public lib.Types TypesDefault { get; init; } = lib.Types.Fields | lib.Types.Props; + public ser.Types TypesDefault { get; init; } = ser.Types.Fields | ser.Types.Props; // How to handle backing fields (might be less relevant for code gen) public BackingFieldNaming Naming { get; init; } = BackingFieldNaming.Regular; @@ -62,9 +62,9 @@ public class TypeStructureAnalyzer private TypeStructureInfo BuildTypeInfo(Type type) { var members = new List(); - var typesTodo = type.GetCustomAttribute(true)?.Types ?? _cfg.TypesDefault; - bool doFields = typesTodo.HasFlag(lib.Types.Fields); - bool doProps = typesTodo.HasFlag(lib.Types.Props); + var typesTodo = type.GetCustomAttribute(true)?.Types ?? _cfg.TypesDefault; + bool doFields = typesTodo.HasFlag(ser.Types.Fields); + bool doProps = typesTodo.HasFlag(ser.Types.Props); // Track processed names to avoid duplicates (e.g., field + prop) var processedNames = new HashSet(); @@ -156,8 +156,8 @@ public class TypeStructureAnalyzer } return ( - actualMi.GetCustomAttribute() != null, - actualMi.GetCustomAttribute() != null, + actualMi.GetCustomAttribute() != null, + actualMi.GetCustomAttribute() != null, propName ); } @@ -170,4 +170,4 @@ public class TypeStructureAnalyzer } // Add GetFilters and FilterField if needed, or simplify as above -} \ No newline at end of file +} diff --git a/ser/CodeGenerator.cs b/lib/CodeGenerator.cs similarity index 100% rename from ser/CodeGenerator.cs rename to lib/CodeGenerator.cs diff --git a/ser/SerializableDictionary.cs_bad b/lib/SerializableDictionary.cs_bad similarity index 100% rename from ser/SerializableDictionary.cs_bad rename to lib/SerializableDictionary.cs_bad diff --git a/ser/VersionFormatter.cs_bad b/lib/VersionFormatter.cs_bad similarity index 100% rename from ser/VersionFormatter.cs_bad rename to lib/VersionFormatter.cs_bad diff --git a/ser/XmlFormatter.cs_bad b/lib/XmlFormatter.cs_bad similarity index 100% rename from ser/XmlFormatter.cs_bad rename to lib/XmlFormatter.cs_bad diff --git a/ser/XmlFormatter2.cs b/lib/XmlFormatter2.cs similarity index 93% rename from ser/XmlFormatter2.cs rename to lib/XmlFormatter2.cs index 0315d63..a60f5a9 100644 --- a/ser/XmlFormatter2.cs +++ b/lib/XmlFormatter2.cs @@ -39,60 +39,6 @@ using System.Net.Sockets; namespace lib { - public interface I_Serialize - { - void OnSerialize() {} - object OnDeserialize( object enclosing ) => this; - } - - - [Flags] - public enum Types - { - Fields = 0b_0001, - Props = 0b_0010, - Implied = 0b_0100, - Explicit = 0b_1000, - - - None = 0b_0000, - Default = Fields, - All = Fields | Props, - } - - public class Ser : Attribute - { - public Types Types { get; set; } = Types.Default; - } - - public class Do : Attribute - { - } - - public class Dont : Attribute - { - } - - public class ChildAttribute : Attribute - { - public string[] Values { get; private set; } - - public ChildAttribute( params string[] values ) - { - this.Values = values; - } - } - - public class ChildFieldsAttribute : ChildAttribute - { - public ChildFieldsAttribute( params string[] values ) : base( values ) { } - } - - public class ChildPropsAttribute : ChildAttribute - { - public ChildPropsAttribute( params string[] values ) : base( values ) { } - } - public enum Datastructure { Invalid, @@ -143,7 +89,7 @@ namespace lib public BackingFieldNaming Naming = BackingFieldNaming.Short; public POD POD = POD.Attributes; - public Types TypesDefault = Types.Fields; + public ser.Types TypesDefault = ser.Types.Fields; } public class XmlFormatter2 : IFormatter @@ -328,7 +274,7 @@ namespace lib { object obj = DeserializeObject( elem, mi, type, existing ); - if( obj is I_Serialize iser ) + if( obj is ser.I_Serialize iser ) { if( _cfg.VerboseLogging ) log.info( $"" ); obj = iser.OnDeserialize( null ); @@ -561,8 +507,8 @@ namespace lib string name = childFi.Name; - var dontAtt = childFi.GetCustomAttributes(); - var doAtt = childFi.GetCustomAttributes(); + var dontAtt = childFi.GetCustomAttributes(); + var doAtt = childFi.GetCustomAttributes(); string propName = ""; @@ -574,9 +520,9 @@ namespace lib var propInfo = narrowType.GetProperty( propName ); - dontAtt = propInfo.GetCustomAttributes(); + dontAtt = propInfo.GetCustomAttributes(); - doAtt = propInfo.GetCustomAttributes(); + doAtt = propInfo.GetCustomAttributes(); } if( dontAtt.Any() ) @@ -639,13 +585,13 @@ namespace lib foreach( var childPi in props ) { string name = childPi.Name; - var dontAtt = childPi.GetCustomAttributes(); + var dontAtt = childPi.GetCustomAttributes(); if( dontAtt.Any() ) { continue; } - var doAtt = childPi.GetCustomAttributes(); + var doAtt = childPi.GetCustomAttributes(); name = refl.TypeToIdentifier( name ); @@ -713,7 +659,7 @@ namespace lib { if( doImpls ) { - if( mi.GetCustomAttribute( true ) == null ) + if( mi.GetCustomAttribute( true ) == null ) return true; } @@ -1353,8 +1299,8 @@ namespace lib foreach( var childFi in fields ) { string name = childFi.Name; - var dontAtt = childFi.GetCustomAttributes(); - var doAtt = childFi.GetCustomAttributes(); + var dontAtt = childFi.GetCustomAttributes(); + var doAtt = childFi.GetCustomAttributes(); string propName = ""; if( name.StartsWith( "<" ) && name.EndsWith( "BackingField" ) ) @@ -1365,8 +1311,8 @@ namespace lib var propInfo = narrowType.GetProperty( propName ); - dontAtt = propInfo.GetCustomAttributes(); - doAtt = propInfo.GetCustomAttributes(); + dontAtt = propInfo.GetCustomAttributes(); + doAtt = propInfo.GetCustomAttributes(); } @@ -1411,7 +1357,7 @@ namespace lib { string name = childPi.Name; - var dontAtt = childPi.GetCustomAttributes(); + var dontAtt = childPi.GetCustomAttributes(); if( dontAtt.Any() ) { continue; @@ -1441,19 +1387,19 @@ namespace lib } - private static void GetFilters( Types TypesDefault, MemberInfo mi, Type type, out bool filterFields, out bool filterProps, out bool doImpls, out bool doFields, out bool doProps, out HashSet whitelistFields, out HashSet whitelistProps ) + private static void GetFilters( ser.Types TypesDefault, MemberInfo mi, Type type, out bool filterFields, out bool filterProps, out bool doImpls, out bool doFields, out bool doProps, out HashSet whitelistFields, out HashSet whitelistProps ) { - var custWLFields = mi?.GetCustomAttribute( true ); - var custWLProps = mi?.GetCustomAttribute( true ); + var custWLFields = mi?.GetCustomAttribute( true ); + var custWLProps = mi?.GetCustomAttribute( true ); filterFields = custWLFields != null; filterProps = custWLProps != null; - var typesTodo = type.GetCustomAttribute( true )?.Types ?? TypesDefault; + var typesTodo = type.GetCustomAttribute( true )?.Types ?? TypesDefault; - doImpls = typesTodo.HasFlag( Types.Implied ); - doFields = filterFields || typesTodo.HasFlag( Types.Fields ); - doProps = filterProps || typesTodo.HasFlag( Types.Props ); + doImpls = typesTodo.HasFlag( ser.Types.Implied ); + doFields = filterFields || typesTodo.HasFlag( ser.Types.Fields ); + doProps = filterProps || typesTodo.HasFlag( ser.Types.Props ); whitelistFields = new( custWLFields?.Values ?? new string[0] ); whitelistProps = new( custWLProps?.Values ?? new string[0] ); } diff --git a/ser/XmlSer.cs b/ser/XmlSer.cs index 89f30f9..b6a744e 100644 --- a/ser/XmlSer.cs +++ b/ser/XmlSer.cs @@ -17,8 +17,72 @@ namespace ser; #region Attributes & Enums (Mostly unchanged, ensure these exist) -// Ensure I_Serialize, Types, lib.Ser, lib.Do, lib.Dont, lib.ChildAttribute, etc., -// exist as you defined them in XmlFormatter2.cs. + +public interface I_Serialize +{ + void OnSerialize() { } + object OnDeserialize( object enclosing ) => this; +} + + +[Flags] +public enum Types +{ + Fields = 0b_0001, + Props = 0b_0010, + Implied = 0b_0100, + Explicit = 0b_1000, + + + None = 0b_0000, + Default = Fields, + All = Fields | Props, +} + +public class Ser : Attribute +{ + public Types Types { get; set; } = Types.Default; +} + +public class Do : Attribute +{ +} + +public class Dont : Attribute +{ +} + + +public class ChildAttribute : Attribute +{ + public string[] Values { get; private set; } + + public ChildAttribute( params string[] values ) + { + this.Values = values; + } +} + +public class ChildFieldsAttribute : ChildAttribute +{ + public ChildFieldsAttribute( params string[] values ) : base( values ) { } +} + +public class ChildPropsAttribute : ChildAttribute +{ + public ChildPropsAttribute( params string[] values ) : base( values ) { } +} + + + +public interface ITypeHandler +{ + bool CanHandle( TypeInfo typeInfo, XmlElement? elem = null ); // Elem needed for Deser + void WriteXml( XmlSer xml, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ); + object? ReadXml( XmlSer xml, XmlElement elem, Type expectedType, object? existing ); +} + + // --- Enums & Records (Slightly adjusted/renamed) --- public enum Datastructure { Tree, Graph } @@ -31,12 +95,10 @@ public record XmlCfg : imm.Recorded public bool Verbose { get; init; } = false; public Datastructure Structure { get; init; } = Datastructure.Tree; public int Version { get; init; } = 2; - public ImmutableDictionary> WLProps { get; init; } = ImmutableDictionary>.Empty; - public ImmutableDictionary> WLFields { get; init; } = ImmutableDictionary>.Empty; public ImmutableDictionary Proxies { get; init; } = ImmutableDictionary.Empty; public BackingFieldNaming Naming { get; init; } = BackingFieldNaming.Short; public POD POD { get; init; } = POD.Attributes; - public lib.Types TypesDefault { get; init; } = lib.Types.Fields; + public ser.Types TypesDefault { get; init; } = ser.Types.Fields; public static XmlCfg Default { get; } = new XmlCfg(); } @@ -44,9 +106,23 @@ public record XmlCfg : imm.Recorded #region Reflection & Metadata Cache +public record DependentMember( + string Name, //Prop or field name + TypeInfo Enclosing, + MemberInfo EnclosingMember +); + +public enum MemberMetaType +{ + Invalid, + Simple, + Composite, +} + public record MemberMeta( - MemberInfo Info, + //Base type. Type Type, + MemberInfo Info, string XmlName, Func GetValue, Action SetValue, @@ -55,7 +131,7 @@ public record MemberMeta( bool HasDont ); -public record TypeSerializationInfo( +public record TypeInfo( Type Type, List Members, bool IsISerializable, @@ -66,12 +142,25 @@ public record TypeSerializationInfo( public class TypeMetaCache { - private readonly ConcurrentDictionary _cache = new(); + private readonly ConcurrentDictionary _cache = new(); private readonly XmlCfg _cfg; public TypeMetaCache( XmlCfg cfg ) => _cfg = cfg; - public TypeSerializationInfo Get( Type type ) => _cache.GetOrAdd( type, BuildTypeInfo ); + public TypeInfo Get( Type type ) + { + // Expanded in anticpation of more complex ways to specify saves/loads + + if( _cache.TryGetValue( type, out var ti ) ) + return ti; + + var children = new HashSet(); + + var tiNew = BuildTypeInfo( type, children ); + _cache.AddOrUpdate( type, tiNew, ( t, ti ) => tiNew ); + + return tiNew; + } // Helper to create accessors (using standard reflection - can be optimized) private static Func CreateGetter( MemberInfo mi ) @@ -92,36 +181,106 @@ public class TypeMetaCache return ( _, _ ) => { }; } - private TypeSerializationInfo BuildTypeInfo( Type type ) - { - var members = new List(); - bool filterFields, filterProps, doImpls, doFields, doProps; - HashSet whitelistFields, whitelistProps; - // Use null for MemberInfo as GetFilters works on Type level here - GetFilters( _cfg.TypesDefault, null, type, out filterFields, out filterProps, out doImpls, out doFields, out doProps, out whitelistFields, out whitelistProps ); + // Helper to create accessors (using standard reflection - can be optimized) + private static Func CreateGetter( MemberInfo mi, Func getter ) + { + if( mi is FieldInfo fi ) + { + var innerGet = fi.GetValue; + + return obj => innerGet( getter( obj ) ); + } + + if( mi is PropertyInfo pi && pi.CanRead ) + { + Func innerGet = pi.GetValue; + + return obj => innerGet( getter( obj ) ); + } + + // return pi.GetValue; + + + return _ => null; + } + + private static Action CreateSetter( MemberInfo mi, Func getter ) + { + if( mi is FieldInfo fi ) + { + Action innerSet = fi.SetValue; + + return ( obj, value ) => innerSet( getter( obj ), value ); + } + + + if( mi is PropertyInfo pi && pi.CanWrite ) + { + Action innerSet = pi.SetValue; + + //return innerSet; + + //var innerSetType = innerSet.GetType(); + //var isArgs = innerSetType.Metho + + + return ( obj, value ) => + { + var leaf = getter( obj ); + innerSet( leaf, value ); + }; + } + + return ( _, _ ) => { }; + } + + + public void AddType( Type type, params string[] children ) + { + var hashChildren = new HashSet( children ); + + BuildTypeInfo( type, hashChildren ); + } + + private TypeInfo BuildTypeInfo( Type type, HashSet children ) + { + if( _cfg.Verbose ) + log.info( $"Building TypeInfo for {type.Name}" ); + + var members = new List(); + bool doImpls, doFields, doProps; + + GetFilters( _cfg.TypesDefault, type, out doImpls, out doFields, out doProps ); + var isImm = typeof( imm.Obj ).IsAssignableFrom( type ); + var typesAtt = type.GetCustomAttribute( true ); + var serTypes = typesAtt?.Types ?? ser.Types.None; + + if( doFields || doImpls ) { foreach( var fi in refl.GetAllFields( type ) ) { - ProcessMember( fi, filterFields, doImpls, whitelistFields, isImm, members ); + ProcessMember( fi, serTypes.HasFlag( ser.Types.Fields ), children, doImpls, isImm, members ); } + + } if( doProps || doImpls ) { foreach( var pi in refl.GetAllProperties( type ) ) { - ProcessMember( pi, filterProps, doImpls, whitelistProps, isImm, members ); + ProcessMember( pi, serTypes.HasFlag( ser.Types.Props ), children, doImpls, isImm, members ); } } var (isProxy, proxyDef) = FindProxy( type ); - return new TypeSerializationInfo( + return new TypeInfo( type, members, typeof( ISerializable ).IsAssignableFrom( type ) && !typeof( Delegate ).IsAssignableFrom( type ), // Exclude Delegates @@ -129,6 +288,7 @@ public class TypeMetaCache isProxy, proxyDef ); + } private (bool, TypeProxy?) FindProxy( Type type ) @@ -145,17 +305,34 @@ public class TypeMetaCache return (false, null); } - private void ProcessMember( MemberInfo mi, bool filter, bool doImpls, HashSet whitelist, bool isImm, List members ) + + + + + + private void ProcessMember( MemberInfo mi, bool doMember, HashSet childrenOverridden, bool doImpls, bool isImm, List members ) { - var (hasDo, hasDont, propName) = GetMemberAttributes( mi, out var actualMiForAtts ); + List children = new( childrenOverridden ); + var (hasDo, hasDont, hasImpl, propName) = GetMemberAttributes( mi, out var actualMiForAtts, children ); if( hasDont ) return; + + // TODO MH Change this to a configurable query(s) if( isImm && ( mi.Name == "MetaStorage" || mi.Name == "Fn" ) ) return; + + if( mi.GetCustomAttribute( true ) != null ) return; + if( !(doMember | hasImpl | hasDo) ) + return; + + + + var miType = ( mi is FieldInfo fi ) ? fi.FieldType : ( (PropertyInfo)mi ).PropertyType; // CHANGED (moved up) + string name = mi.Name; string finalName = name; @@ -166,30 +343,125 @@ public class TypeMetaCache finalName = refl.TypeToIdentifier( finalName ); // Ensure XML-safe name - if( !hasDo && FilterField( filter, doImpls, whitelist, actualMiForAtts, mi.Name ) ) // Filter based on original name + var overiddenName = false; + + var getter = CreateGetter( mi ); + var setter = CreateSetter( mi ); + + var blankHashSet = new HashSet(); + + + //Type realMemberType = miType; + if( hasImpl && children.Any() ) + { + //List specialMembers = new(); + foreach( var childName in children ) + { + var memberInfoArr = miType.GetMember( childName ); + var miFinal = memberInfoArr?.FirstOrDefault(); + if( miFinal == null ) + continue; + + var dependentType = miFinal is FieldInfo fidd ? fidd.FieldType : ( miFinal as PropertyInfo ).PropertyType; + + + bool isPod = Type.GetTypeCode( dependentType ) != TypeCode.Object; + + //ProcessMember( miFinal, blankHashSet, doImpls, isImm, specialMembers ); + + //First this one. We need the old getter for the setter. + setter = CreateSetter( miFinal, getter ); + + //Now wrap the getter itself + getter = CreateGetter( miFinal, getter ); + + var depName = $"{finalName}.{childName}"; + + var memberMeta = new MemberMeta( + dependentType, + miFinal, + depName, + getter, + setter, + isPod && _cfg.POD == POD.Attributes, + hasDo, + hasDont + ); + + members.Add( memberMeta ); + + if( _cfg.Verbose ) + { + log.info( $"{depName} ({mi.Name}) -> {finalName} ({dependentType.Name}) PodAtt: {isPod && _cfg.POD == POD.Attributes}" ); + } + + } + + + return; - var type = ( mi is FieldInfo fi ) ? fi.FieldType : ( (PropertyInfo)mi ).PropertyType; - bool isPod = Type.GetTypeCode( type ) != TypeCode.Object && !typeof( IEnumerable ).IsAssignableFrom( type ); // Simplified POD check + /* + foreach( var childName in children ) + { + var memberInfoArr = miType.GetMember( childName ); + var miFinal = memberInfoArr?.FirstOrDefault(); + if( miFinal != null ) + { + realMemberType = ( miFinal is FieldInfo fin ) ? fin.FieldType : ( miFinal as PropertyInfo ).PropertyType; - members.Add( new MemberMeta( + getter = CreateGetter( miFinal, getter ); + setter = CreateSetter( miFinal, setter ); + } + } + //*/ + } + + { + // Simplified POD check + bool isPod = Type.GetTypeCode( miType ) != TypeCode.Object && !typeof( IEnumerable ).IsAssignableFrom( miType ) || overiddenName; + + members.Add( new MemberMeta( + miType, mi, - type, finalName, - CreateGetter( mi ), - CreateSetter( mi ), + getter, + setter, isPod && _cfg.POD == POD.Attributes, hasDo, hasDont ) ); + + if( _cfg.Verbose ) + { + log.info( $"{mi.Name} ({miType.Name}) -> {finalName} ({miType.Name}) PodAtt: {isPod && _cfg.POD == POD.Attributes}" ); + } + } } - private (bool hasDo, bool hasDont, string propName) GetMemberAttributes( MemberInfo mi, out MemberInfo actualMi ) + + private void ProcessDepedentMember( Type type, HashSet children, List members ) + { + } + + + private (bool hasDo, bool hasDont, bool hasImpl, string propName) GetMemberAttributes( MemberInfo mi, out MemberInfo actualMi, List children ) { actualMi = mi; string propName = ""; bool isBacking = mi.Name.StartsWith( "<" ) && mi.Name.EndsWith( "BackingField" ); + var typesAtt = mi.DeclaringType.GetCustomAttribute( true ); + + var serTypes = typesAtt?.Types ?? ser.Types.None; + + var doImpls = serTypes.HasFlag( ser.Types.Implied ); + + var attDo = actualMi.GetCustomAttribute() != null; + var attDont = actualMi.GetCustomAttribute() != null; + + + if( isBacking && mi is FieldInfo ) { var gtIndex = mi.Name.IndexOf( '>' ); @@ -199,39 +471,30 @@ public class TypeMetaCache actualMi = propInfo; } + var attChildren = actualMi.GetCustomAttribute(); + if( attChildren != null ) + { + children.AddRange( attChildren.Values ); + } + return ( - actualMi.GetCustomAttribute() != null, - actualMi.GetCustomAttribute() != null, +attDo, +attDont, + doImpls, propName ); } // --- These helpers are copied/adapted from XmlFormatter2 --- - private static bool FilterField( bool filter, bool doImpls, HashSet whitelist, MemberInfo mi, string name ) + + private static void GetFilters( ser.Types typesDefault, Type type, out bool doImpls, out bool doFields, out bool doProps ) { - if( doImpls && mi.GetCustomAttribute( true ) == null ) - return true; - if( filter && !whitelist.Contains( refl.TypeToIdentifier( name ) ) ) - return true; // Check against XML-safe name - return false; - } + var typesTodo = type.GetCustomAttribute( true )?.Types ?? typesDefault; - private static void GetFilters( lib.Types typesDefault, MemberInfo? mi, Type type, out bool filterFields, out bool filterProps, out bool doImpls, out bool doFields, out bool doProps, out HashSet whitelistFields, out HashSet whitelistProps ) - { - var custWLFields = mi?.GetCustomAttribute( true ); - var custWLProps = mi?.GetCustomAttribute( true ); - - filterFields = custWLFields != null; - filterProps = custWLProps != null; - - var typesTodo = type.GetCustomAttribute( true )?.Types ?? typesDefault; - - doImpls = typesTodo.HasFlag(lib.Types.Implied ); - doFields = filterFields || typesTodo.HasFlag(lib.Types.Fields ); - doProps = filterProps || typesTodo.HasFlag(lib.Types.Props ); - whitelistFields = new( custWLFields?.Values?.Select( refl.TypeToIdentifier ) ?? Enumerable.Empty() ); - whitelistProps = new( custWLProps?.Values?.Select( refl.TypeToIdentifier ) ?? Enumerable.Empty() ); + doImpls = typesTodo.HasFlag( ser.Types.Implied ); + doFields = typesTodo.HasFlag( ser.Types.Fields ); + doProps = typesTodo.HasFlag( ser.Types.Props ); } } @@ -299,460 +562,6 @@ public class TypeResolver #endregion -#region Type Handlers - -public interface ITypeHandler -{ - bool CanHandle( TypeSerializationInfo typeInfo, XmlElement? elem = null ); // Elem needed for Deser - void WriteXml( XmlSer xml, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ); - object? ReadXml( XmlSer xml, XmlElement elem, Type expectedType, object? existing ); -} - -// --- Primitive Handler --- -public class PrimitiveHandler : ITypeHandler -{ - public bool CanHandle( TypeSerializationInfo ti, XmlElement? elem ) => Type.GetTypeCode( ti.Type ) != TypeCode.Object && !typeof( IEnumerable ).IsAssignableFrom( ti.Type ); - - public object? ReadXml( XmlSer xml, XmlElement elem, Type expectedType, object? existing ) - { - string val = elem.HasAttribute( "v" ) ? elem.GetAttribute( "v" ) : elem.InnerText; - if( val == "null" ) - return null; - - // So this is an interesting one. Why not use the expected type? Well, we know we have - // data in the XML, it just wont convert to what we want. - return xml._resolver.ConvertSimple( val, expectedType ); - } - - public void WriteXml( XmlSer xml, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) - { - if( obj == null ) - { - writer.WriteStartElement( name ); - writer.WriteAttributeString( "v", "null" ); - writer.WriteEndElement(); - return; - } - - bool writeElements = xml._cfg.POD == POD.Elements || forceType || !( writer is XmlTextWriter ); - if( !writeElements && writer is XmlTextWriter tw ) - writeElements = tw.WriteState != WriteState.Element; - - if( writeElements ) - writer.WriteStartElement( name ); - - if( forceType || xml._cfg.POD == POD.Elements ) - { - if( forceType ) - writer.WriteAttributeString( "_.t", obj.GetType().FullName ); - writer.WriteAttributeString( "v", obj.ToString() ); - } - else - { - writer.WriteAttributeString( name, obj.ToString() ); - } - - if( writeElements ) - writer.WriteEndElement(); - } -} - -// --- Proxy Handler --- -public class ProxyHandler : ITypeHandler -{ - public bool CanHandle( TypeSerializationInfo ti, XmlElement? elem ) => ti.IsProxy || ( elem?.HasAttribute( "proxy" ) ?? false ); - - - public object? ReadXml( XmlSer xml, XmlElement elem, Type expectedType, object? existing ) - { - var ti = xml._meta.Get( expectedType ); // Re-get to ensure we have proxy info - if( !elem.HasAttribute( "proxy" ) || !ti.ProxyDef.HasValue ) - { - log.warn( $"Proxy read failed for {expectedType.Name}. Fallback needed." ); - return null; // Should fall back or throw - } - var proxyVal = elem.GetAttribute( "proxy" ); - return ti.ProxyDef.Value.fnDes( expectedType.FullName, proxyVal ); - } - - public void WriteXml( XmlSer xml, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) - { - if( obj == null ) - { xml.GetHandler( typeof( object ) ).WriteXml( xml, writer, null, name, memberType, forceType ); return; } - - var ti = xml._meta.Get( obj.GetType() ); - if( !ti.ProxyDef.HasValue ) - { log.error( "Proxy write called without proxy def!" ); return; } - - writer.WriteStartElement( name ); - var proxyStr = ti.ProxyDef.Value.fnSer( obj ); - writer.WriteAttributeString( "proxy", proxyStr ); - writer.WriteEndElement(); - } -} - -// --- ISerializable Handler --- -public class ISerializableHandler : ITypeHandler -{ - public bool CanHandle( TypeSerializationInfo ti, XmlElement? elem ) => ti.IsISerializable; - - public object? ReadXml( XmlSer xml, XmlElement elem, Type expectedType, object? existing ) - { - // Create/Get instance (needs FormatterServices for ISerializable) - object obj = existing ?? FormatterServices.GetUninitializedObject( expectedType ); - long id = xml.TrackIfGraph( obj, elem ); // Track it - - var serInfo = new SerializationInfo( expectedType, new FormatterConverter() ); - - foreach( XmlNode objNode in elem.ChildNodes ) - { - if( objNode is XmlElement childElem ) - { - string childName = childElem.Name; - Type? childType = xml._resolver.FindType( childElem.GetAttribute( "_.t" ) ); - if( childType != null ) - { - var desValue = xml.ReadNode( childElem, childType, null ); - serInfo.AddValue( childName, desValue, childType ); - } - } - } - - var context = new StreamingContext( StreamingContextStates.All ); // Or use xml.Context - var cons = expectedType.GetConstructor( - BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, - null, new[] { typeof( SerializationInfo ), typeof( StreamingContext ) }, null ); - - if( cons != null ) - { - cons.Invoke( obj, new object[] { serInfo, context } ); - } - else - { - log.error( $"ISerializable type {expectedType.Name} lacks the required constructor." ); - } - - if( obj is IDeserializationCallback cb ) - cb.OnDeserialization( obj ); - - return obj; - } - - public void WriteXml( XmlSer xml, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) - { - if( obj == null ) - { /* Write null */ return; } - if( !( obj is ISerializable serObj ) ) - { /* Error */ return; } - - writer.WriteStartElement( name ); - xml.WriteTypeAttr( writer, memberType, obj.GetType() ); - - if( xml.HandleGraphWrite( writer, obj, out bool first ) ) - { - if( first ) - { - var serInfo = new SerializationInfo( obj.GetType(), new FormatterConverter() ); - var context = new StreamingContext( StreamingContextStates.All ); - serObj.GetObjectData( serInfo, context ); - - foreach( var member in serInfo ) - { - xml.WriteNode( writer, member.Value, refl.TypeToIdentifier( member.Name ), member.ObjectType, true ); // Force type for ISer - } - } - } - writer.WriteEndElement(); - } -} - -// --- Collection Handler --- -public class CollectionHandler : ITypeHandler -{ - public bool CanHandle( TypeSerializationInfo ti, XmlElement? elem ) => - typeof( IEnumerable ).IsAssignableFrom( ti.Type ) && ti.Type != typeof( string ); - - public object? ReadXml( XmlSer xml, XmlElement elem, Type expectedType, object? existing ) - { - // Determine element type - Type elemType = GetElementType( expectedType ); - - // Create a temporary list - var listType = typeof( List<> ).MakeGenericType( elemType ); - var list = (IList)Activator.CreateInstance( listType )!; - - xml.TrackIfGraph( list, elem ); // Track list if graph - - // Populate the list - foreach( XmlNode node in elem.ChildNodes ) - { - if( node is XmlElement childElem ) - { - list.Add( xml.ReadNode( childElem, elemType, null ) ); - } - } - - // Convert to the final expected type (Array, Immutable*, List) - return ConvertToFinalCollection( list, expectedType, elemType ); - } - - public void WriteXml( XmlSer xml, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) - { - if( obj == null ) - { /* Write null */ return; } - if( !( obj is IEnumerable collection ) ) - { /* Error */ return; } - - writer.WriteStartElement( name ); - xml.WriteTypeAttr( writer, memberType, obj.GetType() ); - - if( xml.HandleGraphWrite( writer, obj, out bool first ) ) - { - if( first ) - { - Type elemType = GetElementType( obj.GetType() ); - int i = 0; - foreach( var item in collection ) - { - xml.WriteNode( writer, item, $"i{i++}", elemType, false ); - } - } - } - writer.WriteEndElement(); - } - - private Type GetElementType( Type collectionType ) - { - if( collectionType.IsArray ) - return collectionType.GetElementType()!; - if( collectionType.IsGenericType ) - { - var args = collectionType.GetGenericArguments(); - if( args.Length == 1 ) - return args[0]; - if( args.Length == 2 ) - return typeof( KeyValuePair<,> ).MakeGenericType( args ); - } - return typeof( object ); // Fallback - } - - private object ConvertToFinalCollection( IList list, Type expectedType, Type elemType ) - { - if( expectedType.IsArray ) - { - var arr = Array.CreateInstance( elemType, list.Count ); - list.CopyTo( arr, 0 ); - return arr; - } - if( expectedType.IsGenericType ) - { - var genDef = expectedType.GetGenericTypeDefinition(); - if( genDef == typeof( ImmutableArray<> ) ) - { - var method = typeof( ImmutableArray ).GetMethods() - .First( m => m.Name == "ToImmutableArray" && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.IsGenericType && m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof( IEnumerable<> ) ) - .MakeGenericMethod( elemType ); - return method.Invoke( null, new object[] { list } )!; - } - if( genDef == typeof( ImmutableList<> ) ) - { - var method = typeof( ImmutableList ).GetMethods() - .First( m => m.Name == "ToImmutableList" && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.IsGenericType && m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof( IEnumerable<> ) ) - .MakeGenericMethod( elemType ); - return method.Invoke( null, new object[] { list } )!; - } - // Add more immutable/dictionary handlers here (using MakeImmutableDictionary etc.) - } - return list; // Default to List if no specific match - } -} - - -// --- Object Handler (Default/Complex) --- -public class ObjectHandler : ITypeHandler -{ - public bool CanHandle( TypeSerializationInfo ti, XmlElement? elem ) => true; // Fallback - - public object? ReadXml( XmlSer xml, XmlElement elem, Type expectedType, object? existing ) - { - var actualType = xml._resolver.Resolve( elem, expectedType ); - var ti = xml._meta.Get( actualType ); - - // 1. Get/Create Instance - var (obj, _) = GetOrCreateInstance( xml, elem, actualType, existing ); - if( obj == null ) - return null; - - // Handle graph refs (if already processed) - if( xml._cfg.Structure == Datastructure.Graph && elem.HasAttribute( "ref" ) ) - { - long id = long.Parse( elem.GetAttribute( "ref" ) ); - if( xml._processed.TryGetValue( id, out var processedObj ) ) - return processedObj; - } - - // Track if it's new - xml.TrackIfGraph( obj, elem ); - - // 2. Hydrate - foreach( var memberMeta in ti.Members ) - { - var (valueSource, isAttribute) = FindValueSource( elem, memberMeta.XmlName ); - - if( valueSource != null ) - { - object? memberValue; - object? currentMemberValue = memberMeta.GetValue( obj ); - - if( isAttribute ) - { - memberValue = xml._resolver.ConvertSimple( valueSource.Value!, memberMeta.Type ); - } - else // Child Element - { - memberValue = xml.ReadNode( (XmlElement)valueSource, memberMeta.Type, currentMemberValue ); - } - - // Set value, respecting lib.Do/lib.Dont and pre-hydration - if( ShouldSetValue( memberMeta, existing != null ) ) - { - memberMeta.SetValue( obj, memberValue ); - } - } - } - - // 3. Post-processing - if( obj is lib.I_Serialize iSer ) - obj = iSer.OnDeserialize( null ); - if( ti.IsImm && obj is imm.Obj immObj ) - return immObj.Record( $"From XML {elem.Name}" ); - - return obj; - } - - public void WriteXml( XmlSer xml, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) - { - if( obj == null ) - { /* Write null */ return; } - - writer.WriteStartElement( name ); - xml.WriteTypeAttr( writer, memberType, obj.GetType() ); - var ti = xml._meta.Get( obj.GetType() ); - - if( xml.HandleGraphWrite( writer, obj, out bool first ) ) - { - if( first ) - { - foreach( var memberMeta in ti.Members ) - { - var value = memberMeta.GetValue( obj ); - if( value != null ) - { - // If POD-Attribute, write attribute - if( memberMeta.IsPodAttribute ) - { - try - { - writer.WriteAttributeString( memberMeta.XmlName, value.ToString() ); - } - catch( Exception ex ) - { - log.error( $"Writing Att {memberMeta.XmlName} = [{value}]" ); - } - } - else // Else, write element yeah - { - try - { - xml.WriteNode( writer, value, memberMeta.XmlName, memberMeta.Type, false ); - } - catch( Exception ex ) - { - log.error( $"Writing Node {memberMeta.XmlName} = [{value}]" ); - } - } - } - } - } - } - writer.WriteEndElement(); - } - - private (XmlNode? source, bool isAttribute) FindValueSource( XmlElement parent, string name ) - { - if( parent.HasAttribute( name ) ) - { - return (parent.Attributes[name], true); - } - foreach( XmlNode node in parent.ChildNodes ) - { - if( node.NodeType == XmlNodeType.Element && node.Name == name ) - { - return (node, false); - } - } - return (null, false); - } - - private bool ShouldSetValue( MemberMeta member, bool isHydrating ) - { - // If we have a 'lib.Do' attribute, always set it. - if( member.HasDo ) - return true; - // If we are *not* hydrating (i.e., creating new), always set. - if( !isHydrating ) - return true; - // If we *are* hydrating, only set if it *doesn't* have 'lib.Do' (since we already checked) - // This implies a 'merge' - only setting values that were explicitly marked. - // You might need to refine this based on your exact 'merge' semantics. - // A common approach is to *only* set if 'lib.Do' is present when hydrating. - // Let's assume: Set if New, or if Hydrating AND HasDo. - return !isHydrating || member.HasDo; // Revisit this logic based on desired merge. - // Original `XmlFormatter2` seemed to set unless `lib.Dont` was present, - // and it didn't seem to have strong pre-hydration checks *during* SetValue. - // This needs clarification. For now, let's set unless `lib.Dont`. - // return !member.HasDont; // <-- Simpler, maybe closer? - } - - - private (object? obj, long id) GetOrCreateInstance( XmlSer xml, XmlElement elem, Type type, object? existing ) - { - long id = -1; - bool first = true; - - // Check existing - if( existing != null && type.IsAssignableFrom( existing.GetType() ) ) - { - id = xml._idGen.GetId( existing, out first ); - return (existing, id); - } - - // Create new - object? newObj = null; - try - { - if( type.GetConstructor( Type.EmptyTypes ) != null ) - { - newObj = Activator.CreateInstance( type ); - } - else - { - newObj = FormatterServices.GetUninitializedObject( type ); - } - } - catch( Exception ex ) - { - log.error( $"Failed to create instance of {type.Name}: {ex.Message}" ); - return (null, -1); - } - - id = xml._idGen.GetId( newObj, out first ); - return (newObj, id); - } -} - -#endregion - #region XmlSer (Coordinator) public class XmlSer // : IFormatter @@ -767,11 +576,28 @@ public class XmlSer // : IFormatter internal Dictionary _processed = new(); private string _streamSource = ""; - public XmlSer( XmlCfg? cfg = null ) + public XmlSer( XmlCfg? cfg = null, TypeMetaCache metaCache = null ) { + var isCustomConfig = cfg != null; + _cfg = cfg ?? XmlCfg.Default; - _meta = new TypeMetaCache( _cfg ); + + if( _cfg.Verbose ) + { + log.info( $"Config:" ); + log.info( $" {log.var( _cfg.Verbose )}" ); + log.info( $" {log.var( _cfg.Structure )}" ); + log.info( $" {log.var( _cfg.Version )}" ); + log.info( $" {log.var( _cfg.Naming )}" ); + log.info( $" {log.var( _cfg.POD )}" ); + log.info( $" {log.var( _cfg.TypesDefault )}" ); + } + + + _meta = metaCache ?? new TypeMetaCache( _cfg ); + _resolver = new TypeResolver(); + _handlers = new List { new ProxyHandler(), @@ -780,8 +606,17 @@ public class XmlSer // : IFormatter new CollectionHandler(), new ObjectHandler() // Must be last }; + if( _cfg.Verbose ) + { + log.info( $"Handlers in importance..." ); + foreach( var h in _handlers ) + { + log.info( $" {h.GetType().Name}" ); + } + log.high( "XmlSer Initialized." ); + } } internal ITypeHandler GetHandler( Type t, XmlElement? elem = null ) @@ -871,7 +706,6 @@ public class XmlSer // : IFormatter ReadNode( doc.DocumentElement, typeof( T ), obj ); } - internal object? ReadNode( XmlElement elem, Type expectedType, object? existing ) { if( elem.HasAttribute( "v" ) && elem.GetAttribute( "v" ) == "null" ) @@ -917,6 +751,9 @@ public class XmlSer // : IFormatter internal void WriteNode( XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) { + if( _cfg.Verbose ) + log.info( $"Writing {name} ({memberType}) force: {forceType}" ); + if( obj == null ) { writer.WriteStartElement( name ); @@ -929,7 +766,15 @@ public class XmlSer // : IFormatter var ti = _meta.Get( actualType ); var handler = _handlers.First( h => h.CanHandle( ti ) ); - handler.WriteXml( this, writer, obj, name, memberType, forceType || memberType != actualType ); + try + { + + handler.WriteXml( this, writer, obj, name, memberType, forceType || memberType != actualType ); + } + catch( Exception ex ) + { + log.exception( ex, $"{name}({memberType.Name}) forceType: {forceType}" ); + } } } #endregion diff --git a/ser/XmlSer_Core.cs b/ser/XmlSer_Core.cs new file mode 100644 index 0000000..50a5835 --- /dev/null +++ b/ser/XmlSer_Core.cs @@ -0,0 +1,159 @@ + + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using System.Xml; + +namespace ser; + + +// --- Primitive Handler --- +public partial class PrimitiveHandler : ser.ITypeHandler +{ + public bool CanHandle( TypeInfo ti, XmlElement? elem ) + { + var typeCode = Type.GetTypeCode( ti.Type ); + var typeNotObject = Type.GetTypeCode( ti.Type ) != TypeCode.Object; + var isString = ti.Type == typeof( string ); + return typeNotObject | isString; + } +} + +// --- Proxy Handler --- +public partial class ProxyHandler : ITypeHandler +{ + public bool CanHandle( TypeInfo ti, XmlElement? elem ) => ti.IsProxy || ( elem?.HasAttribute( "proxy" ) ?? false ); +} + +// --- ISerializable Handler --- +public partial class ISerializableHandler : ITypeHandler +{ + public bool CanHandle( TypeInfo ti, XmlElement? elem ) => ti.IsISerializable; +} + +// --- Collection Handler --- +public partial class CollectionHandler : ITypeHandler +{ + public bool CanHandle( TypeInfo ti, XmlElement? elem ) => + typeof( IEnumerable ).IsAssignableFrom( ti.Type ); + + private Type GetElementType( Type collectionType ) + { + if( collectionType.IsArray ) + return collectionType.GetElementType()!; + if( collectionType.IsGenericType ) + { + var args = collectionType.GetGenericArguments(); + if( args.Length == 1 ) + return args[0]; + if( args.Length == 2 ) + return typeof( KeyValuePair<,> ).MakeGenericType( args ); + } + return typeof( object ); // Fallback + } + + private object ConvertToFinalCollection( IList list, Type expectedType, Type elemType ) + { + if( expectedType.IsArray ) + { + var arr = Array.CreateInstance( elemType, list.Count ); + list.CopyTo( arr, 0 ); + return arr; + } + if( expectedType.IsGenericType ) + { + var genDef = expectedType.GetGenericTypeDefinition(); + if( genDef == typeof( ImmutableArray<> ) ) + { + var method = typeof( ImmutableArray ).GetMethods() + .First( m => m.Name == "ToImmutableArray" && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.IsGenericType && m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof( IEnumerable<> ) ) + .MakeGenericMethod( elemType ); + return method.Invoke( null, new object[] { list } )!; + } + if( genDef == typeof( ImmutableList<> ) ) + { + var method = typeof( ImmutableList ).GetMethods() + .First( m => m.Name == "ToImmutableList" && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.IsGenericType && m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof( IEnumerable<> ) ) + .MakeGenericMethod( elemType ); + return method.Invoke( null, new object[] { list } )!; + } + // Add more immutable/dictionary handlers here (using MakeImmutableDictionary etc.) + } + return list; // Default to List if no specific match + } +} + + +// --- Object Handler (Default/Complex) --- +public partial class ObjectHandler : ITypeHandler +{ + public bool CanHandle( TypeInfo ti, XmlElement? elem ) => true; // Fallback + + private (XmlNode? source, bool isAttribute) FindValueSource( XmlElement parent, string name ) + { + if( parent.HasAttribute( name ) ) + { + return (parent.Attributes[name], true); + } + foreach( XmlNode node in parent.ChildNodes ) + { + if( node.NodeType == XmlNodeType.Element && node.Name == name ) + { + return (node, false); + } + } + return (null, false); + } + + private bool ShouldSetValue( MemberMeta member, bool isHydrating ) + { + // [Dont] members are filtered out during metadata generation. + // If a member is present in the metadata and a value is found in the XML, + // we should always set it. This handles both new creation and hydration/merge. + return true; + + } + + + private (object? obj, long id) GetOrCreateInstance( XmlSer xml, XmlElement elem, Type type, object? existing ) + { + long id = -1; + bool first = true; + + // Check existing + if( existing != null && type.IsAssignableFrom( existing.GetType() ) ) + { + id = xml._idGen.GetId( existing, out first ); + return (existing, id); + } + + // Create new + object? newObj = null; + try + { + if( type.GetConstructor( Type.EmptyTypes ) != null ) + { + newObj = Activator.CreateInstance( type ); + } + else + { + newObj = FormatterServices.GetUninitializedObject( type ); + } + } + catch( Exception ex ) + { + log.error( $"Failed to create instance of {type.Name}: {ex.Message}" ); + return (null, -1); + } + + id = xml._idGen.GetId( newObj, out first ); + return (newObj, id); + } +} + diff --git a/ser/XmlSer_Read.cs b/ser/XmlSer_Read.cs new file mode 100644 index 0000000..cf9b99f --- /dev/null +++ b/ser/XmlSer_Read.cs @@ -0,0 +1,188 @@ + + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using System.Xml; + +namespace ser; + + +// --- Primitive Handler --- +public partial class PrimitiveHandler : ser.ITypeHandler +{ + public object? ReadXml( XmlSer xml, XmlElement elem, Type expectedType, object? existing ) + { + string val = elem.HasAttribute( "v" ) ? elem.GetAttribute( "v" ) : elem.InnerText; + if( val == "null" ) + return null; + + // So this is an interesting one. Why not use the expected type? Well, we know we have + // data in the XML, it just wont convert to what we want. + return xml._resolver.ConvertSimple( val, expectedType ); + } +} + +// --- Proxy Handler --- +public partial class ProxyHandler : ITypeHandler +{ + public object? ReadXml( XmlSer xml, XmlElement elem, Type expectedType, object? existing ) + { + var ti = xml._meta.Get( expectedType ); // Re-get to ensure we have proxy info + if( !elem.HasAttribute( "proxy" ) || !ti.ProxyDef.HasValue ) + { + log.warn( $"Proxy read failed for {expectedType.Name}. Fallback needed." ); + return null; // Should fall back or throw + } + var proxyVal = elem.GetAttribute( "proxy" ); + return ti.ProxyDef.Value.fnDes( expectedType.FullName, proxyVal ); + } +} + +// --- ISerializable Handler --- +public partial class ISerializableHandler : ITypeHandler +{ + public object? ReadXml( XmlSer xml, XmlElement elem, Type expectedType, object? existing ) + { + // Create/Get instance (needs FormatterServices for ISerializable) + object obj = existing ?? FormatterServices.GetUninitializedObject( expectedType ); + long id = xml.TrackIfGraph( obj, elem ); // Track it + + var serInfo = new SerializationInfo( expectedType, new FormatterConverter() ); + + foreach( XmlNode objNode in elem.ChildNodes ) + { + if( objNode is XmlElement childElem ) + { + string childName = childElem.Name; + Type? childType = xml._resolver.FindType( childElem.GetAttribute( "_.t" ) ); + if( childType != null ) + { + var desValue = xml.ReadNode( childElem, childType, null ); + serInfo.AddValue( childName, desValue, childType ); + } + } + } + + var context = new StreamingContext( StreamingContextStates.All ); // Or use xml.Context + var cons = expectedType.GetConstructor( + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + null, new[] { typeof( SerializationInfo ), typeof( StreamingContext ) }, null ); + + if( cons != null ) + { + cons.Invoke( obj, new object[] { serInfo, context } ); + } + else + { + log.error( $"ISerializable type {expectedType.Name} lacks the required constructor." ); + } + + if( obj is IDeserializationCallback cb ) + cb.OnDeserialization( obj ); + + return obj; + } +} + +// --- Collection Handler --- +public partial class CollectionHandler : ITypeHandler +{ + + public object? ReadXml( XmlSer xml, XmlElement elem, Type expectedType, object? existing ) + { + // Determine element type + Type elemType = GetElementType( expectedType ); + + // Create a temporary list + var listType = typeof( List<> ).MakeGenericType( elemType ); + var list = (IList)Activator.CreateInstance( listType )!; + + xml.TrackIfGraph( list, elem ); // Track list if graph + + // Populate the list + foreach( XmlNode node in elem.ChildNodes ) + { + if( node is XmlElement childElem ) + { + list.Add( xml.ReadNode( childElem, elemType, null ) ); + } + } + + // Convert to the final expected type (Array, Immutable*, List) + return ConvertToFinalCollection( list, expectedType, elemType ); + } +} + + +// --- Object Handler (Default/Complex) --- +public partial class ObjectHandler : ITypeHandler +{ + public object? ReadXml( XmlSer xml, XmlElement elem, Type expectedType, object? existing ) + { + var actualType = xml._resolver.Resolve( elem, expectedType ); + var ti = xml._meta.Get( actualType ); + + // 1. Get/Create Instance + var (obj, _) = GetOrCreateInstance( xml, elem, actualType, existing ); + if( obj == null ) + return null; + + // Handle graph refs (if already processed) + if( xml._cfg.Structure == Datastructure.Graph && elem.HasAttribute( "ref" ) ) + { + long id = long.Parse( elem.GetAttribute( "ref" ) ); + if( xml._processed.TryGetValue( id, out var processedObj ) ) + return processedObj; + } + + // Track if it's new + xml.TrackIfGraph( obj, elem ); + + // 2. Hydrate + foreach( var memberMeta in ti.Members ) + { + + + + { + var (valueSource, isAttribute) = FindValueSource( elem, memberMeta.XmlName ); + + if( valueSource != null ) + { + object? memberValue; + object? currentMemberValue = memberMeta.GetValue( obj ); + + if( isAttribute ) + { + memberValue = xml._resolver.ConvertSimple( valueSource.Value!, memberMeta.Type ); + } + else // Child Element + { + memberValue = xml.ReadNode( (XmlElement)valueSource, memberMeta.Type, currentMemberValue ); + } + + // Set value, respecting ser.Do/ser.Dont and pre-hydration + if( ShouldSetValue( memberMeta, existing != null ) ) + { + memberMeta.SetValue( obj, memberValue ); + } + } + } + } + + // 3. Post-processing + if( obj is ser.I_Serialize iSer ) + obj = iSer.OnDeserialize( null ); + if( ti.IsImm && obj is imm.Obj immObj ) + return immObj.Record( $"From XML {elem.Name}" ); + + return obj; + } +} + diff --git a/ser/XmlSer_Tests.cs b/ser/XmlSer_Tests.cs new file mode 100644 index 0000000..6d39013 --- /dev/null +++ b/ser/XmlSer_Tests.cs @@ -0,0 +1,133 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Text; +//using Org.BouncyCastle.Crypto.IO; + +namespace ser; + + +public record class SimpleImmutable( string Name, int Age ) : imm.Timed; + +static public class Test +{ + + public class ExternalClass + { + public string ActualProperty { get; set; } = "test_ActualProperty_set_inline"; + //public string ActualProperty_NotSerialized { get; set; } = "ActualProperty_NotSerialized"; + } + + [ser.Ser( Types = ser.Types.Implied )] + public partial class LeaveWithExternalClasss + { + + //[ser.Do] + //public bool _cccwp_doBool = true; + + [ser.ChildPropsAttribute( "ActualProperty" )] + public ExternalClass _leaf_external = new(); + + //public string _cccwp_doNotSerialize = "test_do_not_serialize"; + + } + + [ser.Ser] + public class TrunkClass + { + public LeaveWithExternalClasss _trunk_leaf = new(); + //public int _chf_test = 10; + //private string _chf_priv_string = "test_priv_string"; + }; + + public static void Serialization() + { + ser.XmlCfg cfg = new() + { + Verbose = true, + }; + + ser.TypeMetaCache metaCache = new( cfg ); + //metaCache.AddType( typeof( ClassContainsClassWithProp ), "ActualProperty" ); + + + + + TrunkClass trunk = new() + { + _trunk_leaf = new() + { + _leaf_external = new() + { + ActualProperty = "ActualProperty_set_in_cons" + } + } + }; + + Debug.Assert( trunk._trunk_leaf._leaf_external.ActualProperty == "ActualProperty_set_in_cons" ); + + + var memStream = new MemoryStream(); + + { + var xml = new ser.XmlSer( cfg, metaCache ); + xml.Serialize( memStream, trunk ); + } + + memStream.Position = 0; + + var strXml = System.Text.Encoding.UTF8.GetString( memStream.ToArray() ); + + var badXml = "\n \n"; + ///* + var badXml_02 = @""" + + + + + + + +"""; +//*/ + Debug.Assert( strXml != badXml ); + + memStream.Position = 0; + + var classHasFields2 = new TrunkClass(); + //classHasFields2._chf_prop._cccwp_propHolder.ActualProperty_NotSerialized = "ActualProperty_NotSerialized_set_in_test_01"; + + Debug.Assert( trunk._trunk_leaf._leaf_external.ActualProperty == "test_ActualProperty_set_inline" ); + //Debug.Assert( classHasFields2._chf_prop._cccwp_propHolder.ActualProperty_NotSerialized == "ActualProperty_NotSerialized" ); + + + { + var xml = new ser.XmlSer(cfg, metaCache); + classHasFields2 = xml.Deserialize( memStream ); + } + + + Debug.Assert( trunk._trunk_leaf._leaf_external.ActualProperty == "test_ActualProperty_set_inline" ); + //Debug.Assert( classHasFields2._chf_prop._cccwp_propHolder.ActualProperty_NotSerialized == "ActualProperty_NotSerialized_set_in_test_01" ); + + memStream.Position = 0; + + var classHasFields3 = new TrunkClass(); + + { + var xml = new ser.XmlSer(cfg, metaCache); + xml.DeserializeInto( memStream, classHasFields3 ); + } + + Debug.Assert( trunk._trunk_leaf._leaf_external.ActualProperty == "test_ActualProperty_set_inline" ); + //Debug.Assert( classHasFields3._chf_prop._cccwp_propHolder.ActualProperty_NotSerialized == "ActualProperty_NotSerialized_set_in_test_01" ); + + + } + + + +} + diff --git a/ser/XmlSer_Write.cs b/ser/XmlSer_Write.cs new file mode 100644 index 0000000..9cdb9ff --- /dev/null +++ b/ser/XmlSer_Write.cs @@ -0,0 +1,193 @@ + + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using System.Xml; + +namespace ser; + + +// --- Primitive Handler --- +public partial class PrimitiveHandler : ser.ITypeHandler +{ + public void WriteXml( XmlSer xml, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) + { + if( obj == null ) + { + writer.WriteStartElement( name ); + writer.WriteAttributeString( "v", "null" ); + writer.WriteEndElement(); + return; + } + + bool writeElements = xml._cfg.POD == POD.Elements || forceType || !( writer is XmlTextWriter ); + if( !writeElements && writer is XmlTextWriter tw ) + writeElements = tw.WriteState != WriteState.Element; + + if( writeElements ) + writer.WriteStartElement( name ); + + if( forceType || xml._cfg.POD == POD.Elements ) + { + if( forceType ) + writer.WriteAttributeString( "_.t", obj.GetType().FullName ); + + writer.WriteAttributeString( "v", obj.ToString() ); + } + else + { + writer.WriteAttributeString( name, obj.ToString() ); + } + + if( writeElements ) + writer.WriteEndElement(); + } +} + +// --- Proxy Handler --- +public partial class ProxyHandler : ITypeHandler +{ + public void WriteXml( XmlSer xml, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) + { + if( obj == null ) + { xml.GetHandler( typeof( object ) ).WriteXml( xml, writer, null, name, memberType, forceType ); return; } + + var ti = xml._meta.Get( obj.GetType() ); + if( !ti.ProxyDef.HasValue ) + { log.error( "Proxy write called without proxy def!" ); return; } + + writer.WriteStartElement( name ); + var proxyStr = ti.ProxyDef.Value.fnSer( obj ); + + // TODO: Allow arbitrary writing here + writer.WriteAttributeString( "proxy", proxyStr ); + writer.WriteEndElement(); + } +} + +// --- ISerializable Handler --- +public partial class ISerializableHandler : ITypeHandler +{ + public void WriteXml( XmlSer xml, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) + { + if( obj == null ) + { /* Write null */ return; } + if( !( obj is ISerializable serObj ) ) + { /* Error */ return; } + + writer.WriteStartElement( name ); + xml.WriteTypeAttr( writer, memberType, obj.GetType() ); + + if( xml.HandleGraphWrite( writer, obj, out bool first ) ) + { + if( first ) + { + var serInfo = new SerializationInfo( obj.GetType(), new FormatterConverter() ); + var context = new StreamingContext( StreamingContextStates.All ); + serObj.GetObjectData( serInfo, context ); + + foreach( var member in serInfo ) + { + xml.WriteNode( writer, member.Value, refl.TypeToIdentifier( member.Name ), member.ObjectType, true ); // Force type for ISer + } + } + } + writer.WriteEndElement(); + } +} + +// --- Collection Handler --- +public partial class CollectionHandler : ITypeHandler +{ + public void WriteXml( XmlSer xml, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) + { + if( obj == null ) + { /* Write null */ return; } + if( !( obj is IEnumerable collection ) ) + { /* Error */ return; } + + writer.WriteStartElement( name ); + xml.WriteTypeAttr( writer, memberType, obj.GetType() ); + + if( xml.HandleGraphWrite( writer, obj, out bool first ) ) + { + if( first ) + { + Type elemType = GetElementType( obj.GetType() ); + int i = 0; + foreach( var item in collection ) + { + xml.WriteNode( writer, item, $"i{i++}", elemType, false ); + } + } + } + writer.WriteEndElement(); + } +} + + +// --- Object Handler (Default/Complex) --- +public partial class ObjectHandler : ITypeHandler +{ + public void WriteXml( XmlSer xml, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) + { + if( obj == null ) + { /* Write null */ return; } + + writer.WriteStartElement( name ); + xml.WriteTypeAttr( writer, memberType, obj.GetType() ); + var ti = xml._meta.Get( obj.GetType() ); + + if( xml.HandleGraphWrite( writer, obj, out bool first ) ) + { + if( first ) + { + foreach( var memberMeta in ti.Members ) + { + var value = memberMeta.GetValue( obj ); + if( value != null ) + { + // If POD-Attribute, write attribute + if( memberMeta.IsPodAttribute ) + { + try + { + writer.WriteAttributeString( memberMeta.XmlName, value.ToString() ); + } + catch( Exception ex ) + { + log.error( $"Writing Att {memberMeta.XmlName} = [{value}]" ); + } + } + else + { + try + { + xml.WriteNode( writer, value, memberMeta.XmlName, memberMeta.Type, false ); + } + catch( Exception ex ) + { + log.error( $"Writing Node {memberMeta.XmlName} = [{value}]" ); + } + } + } + + + } + + + + + } + } + + writer.WriteEndElement(); + } +} + diff --git a/tests/Tests.cs b/tests/Tests.cs new file mode 100644 index 0000000..95c9455 --- /dev/null +++ b/tests/Tests.cs @@ -0,0 +1,125 @@ + + + + + + +using System.Diagnostics; +using System.IO; + +namespace test; + + +public record class SimpleImmutable( string Name, int Age ) : imm.Timed; + +static public class XmlFormatter2 +{ + + public class ClassWithProperties + { + public string ActualProperty { get; set; } = "test_ActualProperty_set_inline"; + public string ActualProperty_NotSerialized { get; set; } = "ActualProperty_NotSerialized"; + } + + [ser.Ser( Types = ser.Types.Implied )] + public partial class ClassContainsClassWithProp + { + + [ser.Do] + public bool doBool = true; + + [ser.ChildPropsAttribute( "ActualProperty" )] + public ClassWithProperties propHolder = new(); + + public string doNotSerialize = "test_do_not_serialize"; + + } + + [ser.Ser] + public class ClassHasFields + { + public ClassContainsClassWithProp prop = new(); + }; + + public static void Serialization() + { + + lib.XmlFormatter2Cfg cfg = new() + { + + }; + + + + + ClassHasFields classHasFields = new() + { + prop = new() + { + propHolder = new() + { + ActualProperty = "ActualProperty_set_in_cons" + } + } + }; + + Debug.Assert( classHasFields.prop.propHolder.ActualProperty == "ActualProperty_set_in_cons" ); + + + var memStream = new MemoryStream(); + + { + var xml = new lib.XmlFormatter2 ( cfg ); + xml.Serialize( memStream, classHasFields ); + } + + memStream.Position = 0; + + var strXml = System.Text.Encoding.UTF8.GetString( memStream.ToArray() ); + + var badXml = "\n \n"; + /* + + + + +*/ + Debug.Assert( strXml != badXml ); + + memStream.Position = 0; + + var classHasFields2 = new ClassHasFields(); + classHasFields2.prop.propHolder.ActualProperty_NotSerialized = "ActualProperty_NotSerialized_set_in_test_01"; + + Debug.Assert( classHasFields2.prop.propHolder.ActualProperty == "test_ActualProperty_set_inline" ); + Debug.Assert( classHasFields2.prop.propHolder.ActualProperty_NotSerialized == "ActualProperty_NotSerialized" ); + + + { + var xml = new lib.XmlFormatter2 ( cfg ); + classHasFields2 = xml.Deserialize( memStream ); + } + + + Debug.Assert( classHasFields2.prop.propHolder.ActualProperty == "ActualProperty_set_in_cons" ); + Debug.Assert( classHasFields2.prop.propHolder.ActualProperty_NotSerialized == "ActualProperty_NotSerialized_set_in_test_01" ); + + memStream.Position = 0; + + var classHasFields3 = new ClassHasFields(); + + { + var xml = new lib.XmlFormatter2 ( cfg ); + xml.DeserializeInto( memStream, classHasFields3 ); + } + + Debug.Assert( classHasFields3.prop.propHolder.ActualProperty == "ActualProperty_set_in_cons" ); + Debug.Assert( classHasFields3.prop.propHolder.ActualProperty_NotSerialized == "ActualProperty_NotSerialized_set_in_test_01" ); + + + } + + + +} +