922 lines
27 KiB
C#
922 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 )
|
|
{
|
|
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<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
|