sharplib/ser/XmlSer.cs

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