sharplib/ser/XmlSer.cs
2025-08-17 10:57:44 -07:00

918 lines
27 KiB
C#

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<object, string> fnSer, Func<string, string, object> fnDes );
public record XmlCfg : imm.Recorded<XmlCfg>
{
public bool Verbose { get; init; } = false;
public Datastructure Structure { get; init; } = Datastructure.Tree;
public int Version { get; init; } = 2;
public ImmutableDictionary<string, ImmutableList<string>> WLProps { get; init; } = ImmutableDictionary<string, ImmutableList<string>>.Empty;
public ImmutableDictionary<string, ImmutableList<string>> WLFields { get; init; } = ImmutableDictionary<string, ImmutableList<string>>.Empty;
public ImmutableDictionary<Type, TypeProxy> Proxies { get; init; } = ImmutableDictionary<Type, TypeProxy>.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<object, object?> GetValue,
Action<object, object?> SetValue,
bool IsPodAttribute,
bool HasDo,
bool HasDont
);
public record TypeSerializationInfo(
Type Type,
List<MemberMeta> Members,
bool IsISerializable,
bool IsImm,
bool IsProxy,
TypeProxy? ProxyDef
);
public class TypeMetaCache
{
private readonly ConcurrentDictionary<Type, TypeSerializationInfo> _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<object, object?> 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<object, object?> 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<MemberMeta>();
bool filterFields, filterProps, doImpls, doFields, doProps;
HashSet<string> 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<string> whitelist, bool isImm, List<MemberMeta> 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<NonSerializedAttribute>( 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<lib.Do>() != null,
actualMi.GetCustomAttribute<lib.Dont>() != null,
propName
);
}
// --- These helpers are copied/adapted from XmlFormatter2 ---
private static bool FilterField( bool filter, bool doImpls, HashSet<string> whitelist, MemberInfo mi, string name )
{
if( doImpls && mi.GetCustomAttribute<lib.ChildAttribute>( 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<string> whitelistFields, out HashSet<string> whitelistProps )
{
var custWLFields = mi?.GetCustomAttribute<lib.ChildFieldsAttribute>( true );
var custWLProps = mi?.GetCustomAttribute<lib.ChildPropsAttribute>( true );
filterFields = custWLFields != null;
filterProps = custWLProps != null;
var typesTodo = type.GetCustomAttribute<lib.Ser>( 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<string>() );
whitelistProps = new( custWLProps?.Values?.Select( refl.TypeToIdentifier ) ?? Enumerable.Empty<string>() );
}
}
public class TypeResolver
{
private readonly ConcurrentDictionary<string, Type?> _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 )
{
log.warn( $"Conversion failed for '{value}' to {type.Name}: {ex.Message}. Returning default." );
return type.IsValueType ? Activator.CreateInstance( type ) : null;
}
}
}
#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;
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<T> 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 )
{
writer.WriteAttributeString( memberMeta.XmlName, value.ToString() );
}
else // Else, write element
{
xml.WriteNode( writer, value, memberMeta.XmlName, memberMeta.Type, false );
}
}
}
}
}
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<ITypeHandler> _handlers;
// Per-operation state
internal ObjectIDGenerator _idGen = new();
internal Dictionary<long, object> _processed = new();
private string _streamSource = "";
public XmlSer( XmlCfg? cfg = null )
{
_cfg = cfg ?? XmlCfg.Default;
_meta = new TypeMetaCache( _cfg );
_resolver = new TypeResolver();
_handlers = new List<ITypeHandler>
{
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<T>( 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<T>( 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