- Introduced XmlSer class for XML serialization and deserialization. - Added TypeMetaCache for caching type metadata and reflection information. - Implemented various ITypeHandler implementations for handling different types (Primitive, Proxy, ISerializable, Collection, Object). - Enhanced type resolution with TypeResolver to manage type lookups and conversions. - Established a configuration class (XmlCfg) to manage serialization settings. - Integrated support for handling graphs and references in serialized objects. - Added extensive member processing and filtering based on attributes. - Ensured compatibility with immutable collections and various data structures. - Implemented detailed error handling and logging for serialization processes.
435 lines
13 KiB
C#
435 lines
13 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Reflection;
|
|
using System.Collections.Immutable;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Threading;
|
|
using Microsoft.CodeAnalysis; // Note: This was in the original, but seems unused. Keep for now.
|
|
using System.Linq;
|
|
|
|
#nullable enable
|
|
|
|
namespace res;
|
|
|
|
// A delegate representing a function that can load a resource of type T.
|
|
public delegate T Load<out T>( string filename );
|
|
|
|
/// <summary>
|
|
/// Abstract base class for a resource reference.
|
|
/// Provides a common way to refer to resources and includes debugging/tracking info.
|
|
/// </summary>
|
|
[DebuggerDisplay( "Path = {Filename}" )]
|
|
public abstract class Ref
|
|
{
|
|
public static bool VerboseLogging { get; set; } = false;
|
|
|
|
public string Filename { get; }
|
|
public string Reason { get; }
|
|
public string DbgName { get; }
|
|
public string DbgPath { get; }
|
|
public int DbgLine { get; }
|
|
|
|
protected Ref(
|
|
string filename,
|
|
string reason = "",
|
|
[CallerMemberName] string dbgName = "",
|
|
[CallerFilePath] string dbgPath = "",
|
|
[CallerLineNumber] int dbgLine = 0 )
|
|
{
|
|
Filename = filename;
|
|
Reason = reason;
|
|
DbgName = dbgName;
|
|
DbgPath = dbgPath;
|
|
DbgLine = dbgLine;
|
|
|
|
if( VerboseLogging )
|
|
log.info( $"Ref Created: {GetType().Name} {Filename}" );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Looks up and loads the resource.
|
|
/// </summary>
|
|
/// <returns>The loaded resource object.</returns>
|
|
public abstract object Lookup(
|
|
string reason = "",
|
|
[CallerMemberName] string dbgName = "",
|
|
[CallerFilePath] string dbgPath = "",
|
|
[CallerLineNumber] int dbgLine = 0 );
|
|
|
|
/// <summary>
|
|
/// Called when the resource might have changed (optional, base implementation does nothing).
|
|
/// </summary>
|
|
public virtual void OnChange() { }
|
|
|
|
/// <summary>
|
|
/// Internal method to trigger the initial load (used by deferred loading, if implemented).
|
|
/// </summary>
|
|
internal virtual void InternalLoad() { }
|
|
}
|
|
|
|
/// <summary>
|
|
/// A typed reference to a resource of type T.
|
|
/// Handles lazy loading and access to the resource.
|
|
/// </summary>
|
|
[Serializable]
|
|
[DebuggerDisplay( "Path = {Filename} / Res = {m_res}" )]
|
|
public class Ref<T> : Ref where T : class, new()
|
|
{
|
|
[NonSerialized]
|
|
private T? m_res;
|
|
|
|
/// <summary>
|
|
/// Gets the resource, loading it if necessary.
|
|
/// </summary>
|
|
public T Res => m_res ?? Lookup();
|
|
|
|
/// <summary>
|
|
/// Gets the resource, loading it if necessary.
|
|
/// </summary>
|
|
//[Deprecated("Use Res property instead.")]
|
|
public T res => m_res ?? Lookup();
|
|
|
|
public Ref(
|
|
string filename = "",
|
|
string reason = "",
|
|
[CallerMemberName] string dbgName = "",
|
|
[CallerFilePath] string dbgPath = "",
|
|
[CallerLineNumber] int dbgLine = 0 )
|
|
: base(
|
|
!string.IsNullOrWhiteSpace( filename ) ? filename : $"{{{dbgName}_{Path.GetFileNameWithoutExtension( dbgPath )}}}",
|
|
reason, dbgName, dbgPath, dbgLine )
|
|
{
|
|
if( VerboseLogging )
|
|
log.info( $"Ref<T> Created: {GetType().Name}<{typeof( T ).Name}> {Filename}" );
|
|
m_res = default;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Looks up and loads the resource.
|
|
/// </summary>
|
|
public override T Lookup(
|
|
string reason = "",
|
|
[CallerMemberName] string dbgName = "",
|
|
[CallerFilePath] string dbgPath = "",
|
|
[CallerLineNumber] int dbgLine = 0 )
|
|
{
|
|
// If already loaded, return it.
|
|
if( m_res != null )
|
|
return m_res;
|
|
|
|
// Load using the Mgr.
|
|
m_res = Mgr.Load<T>( Filename, $"Ref lookup (orig: {Reason}) bcs {reason}", dbgName, dbgPath, dbgLine );
|
|
if( VerboseLogging )
|
|
log.info( $"Ref.Lookup: {GetType().Name}<{typeof( T ).Name}> {Filename}" );
|
|
return m_res;
|
|
}
|
|
|
|
/*
|
|
/// <summary>
|
|
/// Overrides the base Lookup to provide a typed result.
|
|
/// </summary>
|
|
public override object Lookup(
|
|
string reason = "",
|
|
[CallerMemberName] string dbgName = "",
|
|
[CallerFilePath] string dbgPath = "",
|
|
[CallerLineNumber] int dbgLine = 0)
|
|
{
|
|
return Lookup(reason, dbgName, dbgPath, dbgLine);
|
|
}
|
|
*/
|
|
|
|
/// <summary>
|
|
/// Internal load implementation.
|
|
/// </summary>
|
|
internal override void InternalLoad()
|
|
{
|
|
if( m_res == null )
|
|
{
|
|
m_res = Mgr.Load<T>( Filename, Reason, DbgName, DbgPath, DbgLine );
|
|
if( VerboseLogging )
|
|
log.info( $"Ref.InternalLoad: {GetType().Name}<{typeof( T ).Name}> {Filename}" );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new resource asset and saves it, returning a Ref to it.
|
|
/// Handles existing files by renaming them.
|
|
/// </summary>
|
|
public static Ref<T> CreateAsset(
|
|
T value, string path,
|
|
string reason = "",
|
|
[CallerMemberName] string dbgName = "",
|
|
[CallerFilePath] string dbgPath = "",
|
|
[CallerLineNumber] int dbgLine = 0 )
|
|
{
|
|
if( File.Exists( path ) )
|
|
{
|
|
log.warn( $"Asset exists: {path}. Renaming before creating." );
|
|
var newPath = $"{path}_{DateTime.Now:yyyyMMdd_HHmmss}";
|
|
File.Move( path, newPath );
|
|
log.warn( $"Renamed existing asset to: {newPath}" );
|
|
}
|
|
|
|
// Here you would typically save 'value' to 'path' using a registered saver or specific logic.
|
|
// Since saving isn't defined, we'll assume it happens externally or add it here if needed.
|
|
// For now, we'll just log and create the Ref.
|
|
log.info( $"Creating asset Ref for {typeof( T ).Name} at {path}." );
|
|
|
|
// We need a way to save 'value' to 'path' before creating the Ref.
|
|
// This part requires a 'Save' function, which isn't in the original.
|
|
// Let's assume you'll add saving logic here.
|
|
// Mgr.Save(value, path); // Example: Needs implementation
|
|
|
|
var immMeta = (value as imm.Obj)?.Meta;
|
|
|
|
|
|
|
|
var createReason = $"CreateAsset: {value.GetType().Name} - {immMeta?.Reason ?? "N/A"}";
|
|
var newRef = new Ref<T>( path, $"{createReason} bcs {reason}", dbgName, dbgPath, dbgLine );
|
|
|
|
// We should make the newRef hold the 'value' immediately,
|
|
// or ensure loading it back gives the same 'value'.
|
|
// Setting m_res directly might be an option, or caching it.
|
|
newRef.m_res = value;
|
|
Mgr.CacheResource( path, value, $"CreateAsset {typeof( T ).Name}" );
|
|
|
|
return newRef;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Holds a weak reference to a cached resource along with metadata.
|
|
/// </summary>
|
|
internal record ResourceHolder<T>( WeakReference<T> WeakRef, string Name, DateTime Captured ) where T : class;
|
|
|
|
/// <summary>
|
|
/// Manages resource loading, caching, and loader registration.
|
|
/// This static class replaces the original `Mgr` instance.
|
|
/// </summary>
|
|
public static class Mgr
|
|
{
|
|
// Internal holder for type-specific loaders.
|
|
private abstract class LoadHolder { public abstract object Load( string filename ); }
|
|
private class LoadHolder<T> : LoadHolder
|
|
{
|
|
private readonly Load<T> _fnLoad;
|
|
public LoadHolder( Load<T> fnLoad ) { _fnLoad = fnLoad; }
|
|
public override object Load( string filename ) => _fnLoad( filename )!;
|
|
}
|
|
|
|
// Cache for resource holders (contains WeakReferences).
|
|
private static class ResCache<T> where T : class, new()
|
|
{
|
|
public static T s_default = new();
|
|
public static ImmutableDictionary<string, ResourceHolder<T>> s_cache = ImmutableDictionary<string, ResourceHolder<T>>.Empty;
|
|
}
|
|
|
|
private static ImmutableDictionary<Type, LoadHolder> s_loaders = ImmutableDictionary<Type, LoadHolder>.Empty;
|
|
// Using ConcurrentDictionary for per-file locking objects.
|
|
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, object> s_loadingLocks = new();
|
|
|
|
/// <summary>
|
|
/// Registers a specific loading function for a type T.
|
|
/// </summary>
|
|
public static void Register<T>( Load<T> loader ) where T : class, new()
|
|
{
|
|
var type = typeof( T );
|
|
if( s_loaders.ContainsKey( type ) )
|
|
{
|
|
log.warn( $"Loader for type {type.Name} is already registered. Overwriting." );
|
|
}
|
|
ImmutableInterlocked.Update( ref s_loaders, d => d.SetItem( type, new LoadHolder<T>( loader ) ) );
|
|
log.info( $"Registered loader for {type.Name}" );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers loaders for all subtypes of T found in loaded assemblies,
|
|
/// assuming they follow the 'res_load' static generic method pattern.
|
|
/// </summary>
|
|
public static void RegisterSub<T>() => RegisterSub( typeof( T ) );
|
|
|
|
/// <summary>
|
|
/// Registers loaders for all subtypes of a base type.
|
|
/// </summary>
|
|
public static void RegisterSub( Type baseType )
|
|
{
|
|
log.info( $"Scanning for subtypes of {baseType.Name} to register loaders..." );
|
|
MethodInfo? genericLoadMethod = baseType.GetMethods( BindingFlags.Public | BindingFlags.Static )
|
|
.FirstOrDefault( mi => mi.Name == "res_load" && mi.IsGenericMethodDefinition );
|
|
|
|
if( genericLoadMethod == null )
|
|
{
|
|
log.error( $"Could not find 'public static T res_load<T>(string filename)' method on {baseType.Name} or its bases." );
|
|
return;
|
|
}
|
|
|
|
foreach( var ass in AppDomain.CurrentDomain.GetAssemblies() )
|
|
{
|
|
try
|
|
{
|
|
foreach( var t in ass.GetTypes() )
|
|
{
|
|
if( t.IsClass && !t.IsAbstract && baseType.IsAssignableFrom( t ) && t != baseType )
|
|
{
|
|
try
|
|
{
|
|
log.debug( $"Found subtype {t.Name}, creating loader." );
|
|
var concreteLoadMethod = genericLoadMethod.MakeGenericMethod( t );
|
|
var loadDelegateType = typeof( Load<> ).MakeGenericType( t );
|
|
var loaderDelegate = Delegate.CreateDelegate( loadDelegateType, concreteLoadMethod );
|
|
|
|
var loadHolderType = typeof( LoadHolder<> ).MakeGenericType( t );
|
|
var loadHolder = Activator.CreateInstance( loadHolderType, loaderDelegate ) as LoadHolder;
|
|
|
|
if( loadHolder != null )
|
|
{
|
|
if( !ImmutableInterlocked.TryAdd( ref s_loaders, t, loadHolder ) )
|
|
{
|
|
log.debug( $"Loader for {t.Name} already exists, skipping." );
|
|
}
|
|
else
|
|
{
|
|
log.info( $"Registered loader for subtype {t.Name}." );
|
|
}
|
|
}
|
|
}
|
|
catch( Exception ex )
|
|
{
|
|
log.error( $"Failed to create loader for subtype {t.Name}: {ex.Message}" );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch( ReflectionTypeLoadException rtle )
|
|
{
|
|
log.warn( $"Could not fully scan assembly {ass.FullName}: {rtle.Message}" );
|
|
}
|
|
catch( Exception ex )
|
|
{
|
|
log.warn( $"Error scanning assembly {ass.FullName}: {ex.Message}" );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Creates a Ref<T> for a given filename.
|
|
/// </summary>
|
|
public static Ref<T> Lookup<T>(
|
|
string filename,
|
|
string reason = "",
|
|
[CallerMemberName] string dbgName = "",
|
|
[CallerFilePath] string dbgPath = "",
|
|
[CallerLineNumber] int dbgLine = 0 ) where T : class, new()
|
|
{
|
|
return new Ref<T>( filename, reason, dbgName, dbgPath, dbgLine );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads a resource, handling caching and thread-safe loading.
|
|
/// </summary>
|
|
public static T Load<T>(
|
|
string filename,
|
|
string reason = "",
|
|
[CallerMemberName] string dbgName = "",
|
|
[CallerFilePath] string dbgPath = "",
|
|
[CallerLineNumber] int dbgLine = 0 ) where T : class, new()
|
|
{
|
|
// 1. Check cache first (no lock)
|
|
if( TryGetFromCache<T>( filename, out var cachedValue ) )
|
|
{
|
|
return cachedValue;
|
|
}
|
|
|
|
// 2. Get a lock specific to this filename and lock it.
|
|
var fileLock = s_loadingLocks.GetOrAdd( filename, _ => new object() );
|
|
lock( fileLock )
|
|
{
|
|
// 3. Double-check cache *inside* the lock
|
|
if( TryGetFromCache<T>( filename, out cachedValue ) )
|
|
{
|
|
return cachedValue;
|
|
}
|
|
|
|
// 4. Perform the actual load
|
|
log.warn( $"Loading {typeof( T ).Name}: {filename} ({reason} at {dbgName}:{dbgLine})" );
|
|
var newValue = ActualLoad<T>( filename, reason, dbgName, dbgPath, dbgLine );
|
|
|
|
// 5. Cache the new value
|
|
CacheResource( filename, newValue, reason );
|
|
|
|
return newValue;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Manually adds or updates a resource in the cache.
|
|
/// </summary>
|
|
internal static void CacheResource<T>( string filename, T resource, string reason ) where T : class, new()
|
|
{
|
|
var weak = new WeakReference<T>( resource );
|
|
var holder = new ResourceHolder<T>( weak, filename, DateTime.Now );
|
|
ImmutableInterlocked.Update( ref ResCache<T>.s_cache, d => d.SetItem( filename, holder ) );
|
|
log.info( $"Cached {typeof( T ).Name}: {filename} ({reason})" );
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Tries to retrieve a resource from the cache.
|
|
/// </summary>
|
|
private static bool TryGetFromCache<T>( string filename, out T value ) where T : class, new()
|
|
{
|
|
if( ResCache<T>.s_cache.TryGetValue( filename, out var holder ) )
|
|
{
|
|
if( holder.WeakRef.TryGetTarget( out var v ) )
|
|
{
|
|
value = v;
|
|
return true;
|
|
}
|
|
log.info( $"{filename} was in cache but dropped (GC'd), needs reloading." );
|
|
}
|
|
value = default!;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The core loading logic. Must be called within a lock.
|
|
/// </summary>
|
|
private static T ActualLoad<T>(
|
|
string filename,
|
|
string reason = "",
|
|
[CallerMemberName] string dbgName = "",
|
|
[CallerFilePath] string dbgPath = "",
|
|
[CallerLineNumber] int dbgLine = 0
|
|
) where T : class, new()
|
|
{
|
|
if( s_loaders.TryGetValue( typeof( T ), out var loaderHolder ) )
|
|
{
|
|
var loadedObject = loaderHolder.Load( filename );
|
|
if( loadedObject is T value )
|
|
{
|
|
var meta = (value as imm.Obj)?.Meta;
|
|
|
|
// If it's an immutable object, record its loading.
|
|
if( value is imm.Obj imm )
|
|
{
|
|
return (T)imm.Record( $"Loading bcs {reason}", dbgName, dbgPath, dbgLine );
|
|
}
|
|
return value;
|
|
}
|
|
else
|
|
{
|
|
log.error( $"Loader for {typeof( T ).Name} returned wrong type: {loadedObject?.GetType().Name}" );
|
|
return ResCache<T>.s_default;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
log.error( $"Loader could not be found for type {typeof( T ).Name} for file {filename}" );
|
|
return ResCache<T>.s_default;
|
|
}
|
|
}
|
|
}
|