781 lines
18 KiB
C#
781 lines
18 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)
|
|
|
|
|
|
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 }
|
|
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<Type, TypeProxy> Proxies { get; init; } = ImmutableDictionary<Type, TypeProxy>.Empty;
|
|
public BackingFieldNaming Naming { get; init; } = BackingFieldNaming.Short;
|
|
public POD POD { get; init; } = POD.Attributes;
|
|
public ser.Types TypesDefault { get; init; } = ser.Types.Fields;
|
|
public static XmlCfg Default { get; } = new XmlCfg();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#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(
|
|
//Base type.
|
|
Type Type,
|
|
MemberInfo Info,
|
|
string XmlName,
|
|
Func<object, object?> GetValue,
|
|
Action<object, object?> SetValue,
|
|
bool IsPodAttribute,
|
|
bool HasDo,
|
|
bool HasDont
|
|
);
|
|
|
|
public record TypeInfo(
|
|
Type Type,
|
|
List<MemberMeta> Members,
|
|
bool IsISerializable,
|
|
bool IsImm,
|
|
bool IsProxy,
|
|
TypeProxy? ProxyDef
|
|
);
|
|
|
|
public class TypeMetaCache
|
|
{
|
|
private readonly ConcurrentDictionary<Type, TypeInfo> _cache = new();
|
|
private readonly XmlCfg _cfg;
|
|
|
|
public TypeMetaCache( XmlCfg cfg ) => _cfg = cfg;
|
|
|
|
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<string>();
|
|
|
|
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<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 ( _, _ ) => { };
|
|
}
|
|
|
|
|
|
// Helper to create accessors (using standard reflection - can be optimized)
|
|
private static Func<object, object?> CreateGetter( MemberInfo mi, Func<object, object?> getter )
|
|
{
|
|
if( mi is FieldInfo fi )
|
|
{
|
|
var innerGet = fi.GetValue;
|
|
|
|
return obj => innerGet( getter( obj ) );
|
|
}
|
|
|
|
if( mi is PropertyInfo pi && pi.CanRead )
|
|
{
|
|
Func<object, object?> innerGet = pi.GetValue;
|
|
|
|
return obj => innerGet( getter( obj ) );
|
|
}
|
|
|
|
// return pi.GetValue;
|
|
|
|
|
|
return _ => null;
|
|
}
|
|
|
|
private static Action<object, object?> CreateSetter( MemberInfo mi, Func<object, object?> getter )
|
|
{
|
|
if( mi is FieldInfo fi )
|
|
{
|
|
Action<object, object?> innerSet = fi.SetValue;
|
|
|
|
return ( obj, value ) => innerSet( getter( obj ), value );
|
|
}
|
|
|
|
|
|
if( mi is PropertyInfo pi && pi.CanWrite )
|
|
{
|
|
Action<object, object?> 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<string>( children );
|
|
|
|
BuildTypeInfo( type, hashChildren );
|
|
}
|
|
|
|
private TypeInfo BuildTypeInfo( Type type, HashSet<string> children )
|
|
{
|
|
if( _cfg.Verbose )
|
|
log.info( $"Building TypeInfo for {type.Name}" );
|
|
|
|
var members = new List<MemberMeta>();
|
|
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<ser.Ser>( true );
|
|
var serTypes = typesAtt?.Types ?? ser.Types.None;
|
|
|
|
|
|
if( doFields || doImpls )
|
|
{
|
|
foreach( var fi in refl.GetAllFields( type ) )
|
|
{
|
|
ProcessMember( fi, serTypes.HasFlag( ser.Types.Fields ), children, doImpls, isImm, members );
|
|
}
|
|
|
|
|
|
}
|
|
|
|
if( doProps || doImpls )
|
|
{
|
|
foreach( var pi in refl.GetAllProperties( type ) )
|
|
{
|
|
ProcessMember( pi, serTypes.HasFlag( ser.Types.Props ), children, doImpls, isImm, members );
|
|
}
|
|
}
|
|
|
|
var (isProxy, proxyDef) = FindProxy( type );
|
|
|
|
return new TypeInfo(
|
|
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 doMember, HashSet<string> childrenOverridden, bool doImpls, bool isImm, List<MemberMeta> members )
|
|
{
|
|
List<string> 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<NonSerializedAttribute>( 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;
|
|
|
|
if( !string.IsNullOrEmpty( propName ) )
|
|
{
|
|
finalName = ( _cfg.Naming == BackingFieldNaming.Short ) ? propName : name;
|
|
}
|
|
|
|
finalName = refl.TypeToIdentifier( finalName ); // Ensure XML-safe name
|
|
|
|
var overiddenName = false;
|
|
|
|
var getter = CreateGetter( mi );
|
|
var setter = CreateSetter( mi );
|
|
|
|
var blankHashSet = new HashSet<string>();
|
|
|
|
|
|
//Type realMemberType = miType;
|
|
if( hasImpl && children.Any() )
|
|
{
|
|
//List<MemberMeta> 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;
|
|
|
|
/*
|
|
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;
|
|
|
|
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,
|
|
finalName,
|
|
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 void ProcessDepedentMember( Type type, HashSet<string> children, List<MemberMeta> members )
|
|
{
|
|
}
|
|
|
|
|
|
private (bool hasDo, bool hasDont, bool hasImpl, string propName) GetMemberAttributes( MemberInfo mi, out MemberInfo actualMi, List<string> children )
|
|
{
|
|
actualMi = mi;
|
|
string propName = "";
|
|
bool isBacking = mi.Name.StartsWith( "<" ) && mi.Name.EndsWith( "BackingField" );
|
|
|
|
var typesAtt = mi.DeclaringType.GetCustomAttribute<ser.Ser>( true );
|
|
|
|
var serTypes = typesAtt?.Types ?? ser.Types.None;
|
|
|
|
var doImpls = serTypes.HasFlag( ser.Types.Implied );
|
|
|
|
var attDo = actualMi.GetCustomAttribute<ser.Do>() != null;
|
|
var attDont = actualMi.GetCustomAttribute<ser.Dont>() != null;
|
|
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
var attChildren = actualMi.GetCustomAttribute<ser.ChildAttribute>();
|
|
if( attChildren != null )
|
|
{
|
|
children.AddRange( attChildren.Values );
|
|
}
|
|
|
|
return (
|
|
attDo,
|
|
attDont,
|
|
doImpls,
|
|
propName
|
|
);
|
|
}
|
|
|
|
|
|
// --- These helpers are copied/adapted from XmlFormatter2 ---
|
|
|
|
private static void GetFilters( ser.Types typesDefault, Type type, out bool doImpls, out bool doFields, out bool doProps )
|
|
{
|
|
var typesTodo = type.GetCustomAttribute<ser.Ser>( true )?.Types ?? typesDefault;
|
|
|
|
doImpls = typesTodo.HasFlag( ser.Types.Implied );
|
|
doFields = typesTodo.HasFlag( ser.Types.Fields );
|
|
doProps = typesTodo.HasFlag( ser.Types.Props );
|
|
}
|
|
}
|
|
|
|
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 )
|
|
{
|
|
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 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, TypeMetaCache metaCache = null )
|
|
{
|
|
var isCustomConfig = cfg != null;
|
|
|
|
_cfg = cfg ?? XmlCfg.Default;
|
|
|
|
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<ITypeHandler>
|
|
{
|
|
new ProxyHandler(),
|
|
new ISerializableHandler(),
|
|
new PrimitiveHandler(),
|
|
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 )
|
|
{
|
|
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( _cfg.Verbose )
|
|
log.info( $"Writing {name} ({memberType}) force: {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 ) );
|
|
|
|
try
|
|
{
|
|
|
|
handler.WriteXml( this, writer, obj, name, memberType, forceType || memberType != actualType );
|
|
}
|
|
catch( Exception ex )
|
|
{
|
|
log.exception( ex, $"{name}({memberType.Name}) forceType: {forceType}" );
|
|
}
|
|
}
|
|
}
|
|
#endregion
|