using System; using System.IO; using System.Xml; using System.Runtime.Serialization; using System.Collections; using System.Collections.Generic; using System.Reflection; using System.Linq; using System.Collections.Immutable; using System.Net.Sockets; using System.Collections.Concurrent; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Linq.Expressions; 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. // --- Enums & Records (Slightly adjusted/renamed) --- public enum Datastructure { Tree, Graph } public enum BackingFieldNaming { Short, Regular } public enum POD { Attributes, Elements } public record struct TypeProxy( Func fnSer, Func fnDes ); 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 static XmlCfg Default { get; } = new XmlCfg(); } #endregion #region Reflection & Metadata Cache public record MemberMeta( MemberInfo Info, Type Type, string XmlName, Func GetValue, Action SetValue, bool IsPodAttribute, bool HasDo, bool HasDont ); public record TypeSerializationInfo( Type Type, List Members, bool IsISerializable, bool IsImm, bool IsProxy, TypeProxy? ProxyDef ); public class TypeMetaCache { private readonly ConcurrentDictionary _cache = new(); private readonly XmlCfg _cfg; public TypeMetaCache( XmlCfg cfg ) => _cfg = cfg; public TypeSerializationInfo Get( Type type ) => _cache.GetOrAdd( type, BuildTypeInfo ); // Helper to create accessors (using standard reflection - can be optimized) private static Func CreateGetter( MemberInfo mi ) { if( mi is FieldInfo fi ) return fi.GetValue; if( mi is PropertyInfo pi && pi.CanRead ) return pi.GetValue; return _ => null; } private static Action CreateSetter( MemberInfo mi ) { if( mi is FieldInfo fi ) return fi.SetValue; if( mi is PropertyInfo pi && pi.CanWrite ) return pi.SetValue; 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 ); var isImm = typeof( imm.Obj ).IsAssignableFrom( type ); if( doFields || doImpls ) { foreach( var fi in refl.GetAllFields( type ) ) { ProcessMember( fi, filterFields, doImpls, whitelistFields, isImm, members ); } } if( doProps || doImpls ) { foreach( var pi in refl.GetAllProperties( type ) ) { ProcessMember( pi, filterProps, doImpls, whitelistProps, isImm, members ); } } var (isProxy, proxyDef) = FindProxy( type ); return new TypeSerializationInfo( type, members, typeof( ISerializable ).IsAssignableFrom( type ) && !typeof( Delegate ).IsAssignableFrom( type ), // Exclude Delegates isImm, isProxy, proxyDef ); } private (bool, TypeProxy?) FindProxy( Type type ) { var tryType = type; while( tryType != null && tryType != typeof( object ) ) { if( _cfg.Proxies.TryGetValue( tryType, out var proxy ) ) { return (true, proxy); } tryType = tryType.BaseType; } return (false, null); } private void ProcessMember( MemberInfo mi, bool filter, bool doImpls, HashSet whitelist, bool isImm, List members ) { var (hasDo, hasDont, propName) = GetMemberAttributes( mi, out var actualMiForAtts ); if( hasDont ) return; if( isImm && ( mi.Name == "MetaStorage" || mi.Name == "Fn" ) ) return; if( mi.GetCustomAttribute( true ) != null ) return; string name = mi.Name; string finalName = name; if( !string.IsNullOrEmpty( propName ) ) { finalName = ( _cfg.Naming == BackingFieldNaming.Short ) ? propName : name; } finalName = refl.TypeToIdentifier( finalName ); // Ensure XML-safe name if( !hasDo && FilterField( filter, doImpls, whitelist, actualMiForAtts, mi.Name ) ) // Filter based on original name 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 members.Add( new MemberMeta( mi, type, finalName, CreateGetter( mi ), CreateSetter( mi ), isPod && _cfg.POD == POD.Attributes, hasDo, hasDont ) ); } private (bool hasDo, bool hasDont, string propName) GetMemberAttributes( MemberInfo mi, out MemberInfo actualMi ) { actualMi = mi; string propName = ""; bool isBacking = mi.Name.StartsWith( "<" ) && mi.Name.EndsWith( "BackingField" ); if( isBacking && mi is FieldInfo ) { var gtIndex = mi.Name.IndexOf( '>' ); propName = mi.Name.Substring( 1, gtIndex - 1 ); var propInfo = mi.DeclaringType?.GetProperty( propName ); if( propInfo != null ) actualMi = propInfo; } return ( actualMi.GetCustomAttribute() != null, actualMi.GetCustomAttribute() != null, propName ); } // --- These helpers are copied/adapted from XmlFormatter2 --- private static bool FilterField( bool filter, bool doImpls, HashSet whitelist, MemberInfo mi, string name ) { if( doImpls && mi.GetCustomAttribute( true ) == null ) return true; if( filter && !whitelist.Contains( refl.TypeToIdentifier( name ) ) ) return true; // Check against XML-safe name return false; } 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() ); } } public class TypeResolver { private readonly ConcurrentDictionary _cache = new(); private readonly Assembly[] _assemblies; private static readonly FormatterConverter _conv = new(); public TypeResolver() { _assemblies = AppDomain.CurrentDomain.GetAssemblies(); } public Type Resolve( XmlElement elem, Type? expectedType ) { if( elem.HasAttribute( "_.t" ) ) { var typeName = elem.GetAttribute( "_.t" ); var resolved = FindType( typeName ); if( resolved != null ) return resolved; } return expectedType ?? typeof( object ); // Fallback needed } public Type? FindType( string typeName ) { return _cache.GetOrAdd( typeName, tn => { // Try direct lookup first (might work for fully qualified) var t = Type.GetType( tn ); if( t != null ) return t; // Then search assemblies foreach( Assembly a in _assemblies ) { t = a.GetType( tn ); if( t != null ) return t; } log.warn( $"Could not resolve type: {tn}" ); return null; } ); } public object ConvertSimple( string value, Type type ) { if( type.IsEnum ) return Enum.Parse( type, value ); try { return _conv.Convert( value, type ); } catch( Exception ex ) { object defaultVal = type.IsValueType ? Activator.CreateInstance( type )! : null!; log.warn( $"Conversion failed for '{value}' to {type.Name}: {ex.Message}. Returning default of {defaultVal}({defaultVal.GetType().Name})." ); return defaultVal; } } } #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 { 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 { internal readonly XmlCfg _cfg; internal readonly TypeMetaCache _meta; internal readonly TypeResolver _resolver; private readonly List _handlers; // Per-operation state internal ObjectIDGenerator _idGen = new(); internal Dictionary _processed = new(); private string _streamSource = ""; public XmlSer( XmlCfg? cfg = null ) { _cfg = cfg ?? XmlCfg.Default; _meta = new TypeMetaCache( _cfg ); _resolver = new TypeResolver(); _handlers = new List { new ProxyHandler(), new ISerializableHandler(), new PrimitiveHandler(), new CollectionHandler(), new ObjectHandler() // Must be last }; if( _cfg.Verbose ) log.high( "XmlSer Initialized." ); } internal ITypeHandler GetHandler( Type t, XmlElement? elem = null ) { var ti = _meta.Get( t ); return _handlers.First( h => h.CanHandle( ti, elem ) ); } // --- Context Helpers --- internal void WriteTypeAttr( XmlWriter writer, Type memberType, Type actualType ) { if( memberType != actualType ) { writer.WriteAttributeString( "_.t", actualType.FullName ); } } internal bool HandleGraphWrite( XmlWriter writer, object obj, out bool first ) { first = true; if( _cfg.Structure == Datastructure.Graph ) { long id = _idGen.GetId( obj, out first ); writer.WriteAttributeString( "ref", id.ToString() ); if( first ) _processed[id] = obj; } return first || _cfg.Structure == Datastructure.Tree; // Write if first or if Tree } internal long TrackIfGraph( object obj, XmlElement elem ) { long id = -1; bool first; if( _cfg.Structure == Datastructure.Graph ) { id = _idGen.GetId( obj, out first ); if( elem.HasAttribute( "ref" ) ) { id = long.Parse( elem.GetAttribute( "ref" ) ); } if( !_processed.ContainsKey( id ) ) { _processed[id] = obj; } } return id; } // --- Deserialization --- public T? Deserialize( Stream stream ) => (T?)Deserialize( stream, typeof( T ) ); public object? Deserialize( Stream stream, Type? type = null ) { _streamSource = stream.ToString() ?? "{null}"; // Basic source, improve as needed _processed.Clear(); _idGen = new ObjectIDGenerator(); using var reader = XmlReader.Create( stream, new XmlReaderSettings { IgnoreWhitespace = true } ); XmlDocument doc = new XmlDocument(); try { doc.Load( reader ); } catch( Exception ex ) { log.error( $"XML Load failed: {ex.Message}" ); return null; } if( doc.DocumentElement == null ) return null; return ReadNode( doc.DocumentElement, type ?? typeof( object ), null ); } public void DeserializeInto( Stream stream, T obj ) where T : class { _streamSource = stream.ToString() ?? "{null}"; _processed.Clear(); _idGen = new ObjectIDGenerator(); using var reader = XmlReader.Create( stream, new XmlReaderSettings { IgnoreWhitespace = true } ); XmlDocument doc = new XmlDocument(); try { doc.Load( reader ); } catch( Exception ex ) { log.error( $"XML Load failed: {ex.Message}" ); return; } if( doc.DocumentElement == null ) return; ReadNode( doc.DocumentElement, typeof( T ), obj ); } internal object? ReadNode( XmlElement elem, Type expectedType, object? existing ) { if( elem.HasAttribute( "v" ) && elem.GetAttribute( "v" ) == "null" ) return null; // 1. Handle refs (if Graph) if( _cfg.Structure == Datastructure.Graph && elem.HasAttribute( "ref" ) ) { long id = long.Parse( elem.GetAttribute( "ref" ) ); if( _processed.TryGetValue( id, out var obj ) ) return obj; } // 2. Determine Type & Select Handler var actualType = _resolver.Resolve( elem, expectedType ); var ti = _meta.Get( actualType ); var handler = _handlers.First( h => h.CanHandle( ti, elem ) ); // 3. Delegate return handler.ReadXml( this, elem, actualType, existing ); } // --- Serialization --- public void Serialize( Stream stream, object root ) { _processed.Clear(); _idGen = new ObjectIDGenerator(); var settings = new XmlWriterSettings { Indent = true, Encoding = System.Text.Encoding.UTF8, // Use UTF8 for better compatibility OmitXmlDeclaration = true // Often preferred for fragments/storage }; using var writer = XmlWriter.Create( stream, settings ); writer.WriteStartDocument(); WriteNode( writer, root, "root", root?.GetType() ?? typeof( object ), true ); // Force type on root writer.WriteEndDocument(); writer.Flush(); } internal void WriteNode( XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) { if( obj == null ) { writer.WriteStartElement( name ); writer.WriteAttributeString( "v", "null" ); writer.WriteEndElement(); return; } var actualType = obj.GetType(); var ti = _meta.Get( actualType ); var handler = _handlers.First( h => h.CanHandle( ti ) ); handler.WriteXml( this, writer, obj, name, memberType, forceType || memberType != actualType ); } } #endregion