Implement Xml Serialization Framework with Type Handling and Metadata Caching

- 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.
This commit is contained in:
Marc Hernandez 2025-05-28 10:46:00 -07:00
parent f873c42cbf
commit 3f850cc9b0
13 changed files with 2472 additions and 1111 deletions

551
.editorconfig Normal file
View File

@ -0,0 +1,551 @@
# Version: 4.1.1 (Using https://semver.org/)
# Updated: 2022-05-23
# See https://github.com/RehanSaeed/EditorConfig/releases for release notes.
# See https://github.com/RehanSaeed/EditorConfig for updates to this file.
# See http://EditorConfig.org for more information about .editorconfig files.
##########################################
# Common Settings
##########################################
# This file is the top-most EditorConfig file
root = true
# All Files
[*]
charset = utf-8
indent_style = tab
# indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
##########################################
# File Extension Settings
##########################################
# Visual Studio Solution Files
[*.sln]
indent_style = tab
# Visual Studio XML Project Files
[*.{csproj,vbproj,vcxproj.filters,proj,projitems,shproj}]
indent_size = 2
# XML Configuration Files
[*.{xml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct}]
indent_size = 2
# JSON Files
[*.{json,json5,webmanifest}]
indent_size = 2
# YAML Files
[*.{yml,yaml}]
indent_size = 2
# Markdown Files
[*.{md,mdx}]
trim_trailing_whitespace = false
# Web Files
[*.{htm,html,js,jsm,ts,tsx,cjs,cts,ctsx,mjs,mts,mtsx,css,sass,scss,less,pcss,svg,vue}]
indent_size = 2
# Batch Files
[*.{cmd,bat}]
end_of_line = crlf
# Bash Files
[*.sh]
end_of_line = lf
# Makefiles
[Makefile]
indent_style = tab
##########################################
# Default .NET Code Style Severities
# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/configuration-options#scope
##########################################
[*.{cs,csx,cake,vb,vbx}]
# Default Severity for all .NET Code Style rules below
dotnet_analyzer_diagnostic.severity = none
##########################################
# Language Rules
# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/language-rules
##########################################
# .NET Style Rules
# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/language-rules#net-style-rules
[*.{cs,csx,cake,vb,vbx}]
indent_style = tab
# "this." and "Me." qualifiers
dotnet_style_qualification_for_field = false:none
dotnet_style_qualification_for_property = false:none
dotnet_style_qualification_for_method = false:none
dotnet_style_qualification_for_event = false:none
# Language keywords instead of framework type names for type references
dotnet_style_predefined_type_for_locals_parameters_members = true:none
dotnet_style_predefined_type_for_member_access = true:none
# Modifier preferences
dotnet_style_require_accessibility_modifiers = always:none
csharp_preferred_modifier_order = new,static,private,virtual,public,protected,internal,extern,abstract,sealed,override,readonly,unsafe,volatile,async:none
visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:none
dotnet_style_readonly_field = true:none
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:none
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:none
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:none
# Expression-level preferences
dotnet_style_object_initializer = true:none
dotnet_style_collection_initializer = true:none
dotnet_style_explicit_tuple_names = true:none
dotnet_style_prefer_inferred_tuple_names = true:none
dotnet_style_prefer_inferred_anonymous_type_member_names = true:none
dotnet_style_prefer_auto_properties = true:none
dotnet_style_prefer_conditional_expression_over_assignment = false:suggestion
dotnet_diagnostic.IDE0045.severity = suggestion
dotnet_style_prefer_conditional_expression_over_return = false:suggestion
dotnet_diagnostic.IDE0046.severity = suggestion
dotnet_style_prefer_compound_assignment = true:none
dotnet_style_prefer_simplified_interpolation = true:none
dotnet_style_prefer_simplified_boolean_expressions = true:none
# Null-checking preferences
dotnet_style_coalesce_expression = true:none
dotnet_style_null_propagation = true:none
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:none
# File header preferences
# file_header_template = <copyright file="{fileName}" company="PROJECT-AUTHOR">\n© PROJECT-AUTHOR\n</copyright>
# If you use StyleCop, you'll need to disable SA1636: File header copyright text should match.
# dotnet_diagnostic.SA1636.severity = none
# Undocumented
dotnet_style_operator_placement_when_wrapping = end_of_line:none
csharp_style_prefer_null_check_over_type_check = true:none
# C# Style Rules
# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/language-rules#c-style-rules
[*.{cs,csx,cake}]
# 'var' preferences
csharp_style_var_for_built_in_types = true:none
csharp_style_var_when_type_is_apparent = true:none
csharp_style_var_elsewhere = false
# Expression-bodied members
csharp_style_expression_bodied_methods = true:none
csharp_style_expression_bodied_constructors = true:none
csharp_style_expression_bodied_operators = true:none
csharp_style_expression_bodied_properties = true:none
csharp_style_expression_bodied_indexers = true:none
csharp_style_expression_bodied_accessors = true:none
csharp_style_expression_bodied_lambdas = true:none
csharp_style_expression_bodied_local_functions = true:none
# Pattern matching preferences
csharp_style_pattern_matching_over_is_with_cast_check = true:none
csharp_style_pattern_matching_over_as_with_null_check = true:none
csharp_style_prefer_switch_expression = true:none
csharp_style_prefer_pattern_matching = true:none
csharp_style_prefer_not_pattern = true:none
# Expression-level preferences
csharp_style_inlined_variable_declaration = true:none
csharp_prefer_simple_default_expression = true:none
csharp_style_pattern_local_over_anonymous_function = true:none
csharp_style_deconstructed_variable_declaration = true:none
csharp_style_prefer_index_operator = true:none
csharp_style_prefer_range_operator = true:none
csharp_style_implicit_object_creation_when_type_is_apparent = true:none
# "Null" checking preferences
csharp_style_throw_expression = true:none
csharp_style_conditional_delegate_call = true:none
# Code block preferences
csharp_prefer_braces = true:none
csharp_prefer_simple_using_statement = true:suggestion
dotnet_diagnostic.IDE0063.severity = suggestion
# 'using' directive preferences
# csharp_using_directive_placement = inside_namespace:none
# Modifier preferences
csharp_prefer_static_local_function = true:none
##########################################
# Unnecessary Code Rules
# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/unnecessary-code-rules
##########################################
# .NET Unnecessary code rules
[*.{cs,csx,cake,vb,vbx}]
dotnet_code_quality_unused_parameters = all:none
dotnet_remove_unnecessary_suppression_exclusions = none:none
# C# Unnecessary code rules
[*.{cs,csx,cake}]
csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion
dotnet_diagnostic.IDE0058.severity = suggestion
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
dotnet_diagnostic.IDE0059.severity = suggestion
##########################################
# Formatting Rules
# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules
##########################################
# .NET formatting rules
# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#net-formatting-rules
[*.{cs,csx,cake,vb,vbx}]
# Organize using directives
dotnet_sort_system_directives_first = false
dotnet_separate_import_directive_groups = false
# Dotnet namespace options
dotnet_style_namespace_match_folder = true:suggestion
dotnet_diagnostic.IDE0130.severity = suggestion
# C# formatting rules
# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#c-formatting-rules
[*.{cs,csx,cake}]
# Newline options
# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#new-line-options
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_between_query_expression_clauses = true
# Indentation options
# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#indentation-options
csharp_indent_case_contents = true
csharp_indent_switch_labels = true
csharp_indent_labels = no_change
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents_when_block = false
# Spacing options
# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#spacing-options
csharp_space_after_cast = false
csharp_space_after_keywords_in_control_flow_statements = false
csharp_space_between_parentheses = control_flow_statements, expressions
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_around_binary_operators = no_change
csharp_space_between_method_declaration_parameter_list_parentheses = true
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = true
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_after_comma = true
csharp_space_before_comma = false
csharp_space_after_dot = false
csharp_space_before_dot = false
csharp_space_after_semicolon_in_for_statement = true
csharp_space_before_semicolon_in_for_statement = false
csharp_space_around_declaration_statements = false
csharp_space_before_open_square_brackets = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_square_brackets = false
# Wrap options
# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#wrap-options
csharp_preserve_single_line_statements = false
csharp_preserve_single_line_blocks = true
# Namespace options
# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#namespace-options
csharp_style_namespace_declarations = file_scoped:none
##########################################
# .NET Naming Rules
# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/naming-rules
##########################################
[*.{cs,csx,cake,vb,vbx}]
##########################################
# Styles
##########################################
# camel_case_style - Define the camelCase style
dotnet_naming_style.camel_case_style.capitalization = camel_case
# pascal_case_style - Define the PascalCase style
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# first_upper_style - The first character must start with an upper-case character
dotnet_naming_style.first_upper_style.capitalization = first_word_upper
# prefix_interface_with_i_style - Interfaces must be PascalCase and the first character of an interface must be an 'I'
# dotnet_naming_style.prefix_interface_with_i_style.capitalization = pascal_case
# dotnet_naming_style.prefix_interface_with_i_style.required_prefix = I
# prefix_type_parameters_with_t_style - Generic Type Parameters must be PascalCase and the first character must be a 'T'
dotnet_naming_style.prefix_type_parameters_with_t_style.capitalization = pascal_case
dotnet_naming_style.prefix_type_parameters_with_t_style.required_prefix = T
# disallowed_style - Anything that has this style applied is marked as disallowed
dotnet_naming_style.disallowed_style.capitalization = pascal_case
dotnet_naming_style.disallowed_style.required_prefix = ____RULE_VIOLATION____
dotnet_naming_style.disallowed_style.required_suffix = ____RULE_VIOLATION____
# internal_error_style - This style should never occur... if it does, it indicates a bug in file or in the parser using the file
dotnet_naming_style.internal_error_style.capitalization = pascal_case
dotnet_naming_style.internal_error_style.required_prefix = ____INTERNAL_ERROR____
dotnet_naming_style.internal_error_style.required_suffix = ____INTERNAL_ERROR____
##########################################
# .NET Design Guideline Field Naming Rules
# Naming rules for fields follow the .NET Framework design guidelines
# https://docs.microsoft.com/dotnet/standard/design-guidelines/index
##########################################
# All public/protected/protected_internal constant fields must be PascalCase
# https://docs.microsoft.com/dotnet/standard/design-guidelines/field
dotnet_naming_symbols.public_protected_constant_fields_group.applicable_accessibilities = public, protected, protected_internal
dotnet_naming_symbols.public_protected_constant_fields_group.required_modifiers = const
dotnet_naming_symbols.public_protected_constant_fields_group.applicable_kinds = field
dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.symbols = public_protected_constant_fields_group
dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.style = pascal_case_style
dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.severity = none
# All public/protected/protected_internal static readonly fields must be PascalCase
# https://docs.microsoft.com/dotnet/standard/design-guidelines/field
dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_accessibilities = public, protected, protected_internal
dotnet_naming_symbols.public_protected_static_readonly_fields_group.required_modifiers = static, readonly
dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_kinds = field
dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.symbols = public_protected_static_readonly_fields_group
dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style
dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.severity = none
# No other public/protected/protected_internal fields are allowed
# https://docs.microsoft.com/dotnet/standard/design-guidelines/field
#dotnet_naming_symbols.other_public_protected_fields_group.applicable_accessibilities = public, protected, protected_internal
#dotnet_naming_symbols.other_public_protected_fields_group.applicable_kinds = field
#dotnet_naming_rule.other_public_protected_fields_disallowed_rule.symbols = other_public_protected_fields_group
#dotnet_naming_rule.other_public_protected_fields_disallowed_rule.style = disallowed_style
#dotnet_naming_rule.other_public_protected_fields_disallowed_rule.severity = error
##########################################
# StyleCop Field Naming Rules
# Naming rules for fields follow the StyleCop analyzers
# This does not override any rules using disallowed_style above
# https://github.com/DotNetAnalyzers/StyleCopAnalyzers
##########################################
# All constant fields must be PascalCase
# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1303.md
dotnet_naming_symbols.stylecop_constant_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private
dotnet_naming_symbols.stylecop_constant_fields_group.required_modifiers = const
dotnet_naming_symbols.stylecop_constant_fields_group.applicable_kinds = field
dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.symbols = stylecop_constant_fields_group
dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.style = prefix_underscore
dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.severity = none
# All static readonly fields must be PascalCase
# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1311.md
dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private
dotnet_naming_symbols.stylecop_static_readonly_fields_group.required_modifiers = static, readonly
dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_kinds = field
dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.symbols = stylecop_static_readonly_fields_group
dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.style = prefix_underscore
dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.severity = none
# No non-private instance fields are allowed
# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1401.md
dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected
dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_kinds = field
dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.symbols = stylecop_fields_must_be_private_group
dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.style = disallowed_style
dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.severity = error
dotnet_naming_symbols.stylecop_all_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private
#dotnet_naming_symbols.stylecop_all_fields_group.applicable_accessibilities = *
dotnet_naming_symbols.stylecop_all_fields_group.applicable_kinds = field
dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.symbols = stylecop_all_fields_group
dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.style = prefix_underscore
dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.severity = none
dotnet_naming_style.prefix_underscore.capitalization = camel_case
dotnet_naming_style.prefix_underscore.required_prefix = _
# Private fields must be camelCase
# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1306.md
dotnet_naming_symbols.stylecop_static_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private
dotnet_naming_symbols.stylecop_static_fields_group.required_modifiers = static
dotnet_naming_symbols.stylecop_static_fields_group.applicable_kinds = field
dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.symbols = stylecop_static_fields_group
dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.style = prefix_s_underscore
dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.severity = none
dotnet_naming_style.prefix_s_underscore.capitalization = camel_case
dotnet_naming_style.prefix_s_underscore.required_prefix = s_
# Local variables must be camelCase
# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1312.md
dotnet_naming_symbols.stylecop_local_fields_group.applicable_accessibilities = local
dotnet_naming_symbols.stylecop_local_fields_group.applicable_kinds = local
dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.symbols = stylecop_local_fields_group
dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.style = camel_case_style
dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.severity = silent
d
# This rule should never fire. However, it's included for at least two purposes:
# First, it helps to understand, reason about, and root-case certain types of issues, such as bugs in .editorconfig parsers.
# Second, it helps to raise immediate awareness if a new field type is added (as occurred recently in C#).
#dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_accessibilities = *
#dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_kinds = field
#dotnet_naming_rule.sanity_check_uncovered_field_case_rule.symbols = sanity_check_uncovered_field_case_group
#dotnet_naming_rule.sanity_check_uncovered_field_case_rule.style = internal_error_style
#dotnet_naming_rule.sanity_check_uncovered_field_case_rule.severity = error
##########################################
# Other Naming Rules
##########################################
# All of the following must be PascalCase:
# - Namespaces
# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-namespaces
# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md
# - Classes and Enumerations
# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces
# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md
# - Delegates
# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces#names-of-common-types
# - Constructors, Properties, Events, Methods
# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-type-members
dotnet_naming_symbols.element_group.applicable_kinds = namespace, class, enum, struct, delegate, event, method, property
dotnet_naming_rule.element_rule.symbols = element_group
dotnet_naming_rule.element_rule.style = pascal_case_style
dotnet_naming_rule.element_rule.severity = none
# Interfaces use PascalCase and are prefixed with uppercase 'I'
# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces
dotnet_naming_symbols.interface_group.applicable_kinds = interface
dotnet_naming_rule.interface_rule.symbols = interface_group
dotnet_naming_rule.interface_rule.style = camel_case
dotnet_naming_rule.interface_rule.severity = none
# Generics Type Parameters use PascalCase and are prefixed with uppercase 'T'
# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces
dotnet_naming_symbols.type_parameter_group.applicable_kinds = type_parameter
dotnet_naming_rule.type_parameter_rule.symbols = type_parameter_group
dotnet_naming_rule.type_parameter_rule.style = prefix_type_parameters_with_t_style
dotnet_naming_rule.type_parameter_rule.severity = none
# Function parameters use camelCase
# https://docs.microsoft.com/dotnet/standard/design-guidelines/naming-parameters
dotnet_naming_symbols.parameters_group.applicable_kinds = parameter
dotnet_naming_rule.parameters_rule.symbols = parameters_group
dotnet_naming_rule.parameters_rule.style = camel_case_style
dotnet_naming_rule.parameters_rule.severity = none
##########################################
# Chickensoft Rule Overrides
##########################################
# Allow using keywords as names
# dotnet_diagnostic.CA1716.severity = none
# Don't require culture info for ToString()
dotnet_diagnostic.CA1304.severity = none
# Don't require a string comparison for comparing strings.
dotnet_diagnostic.CA1310.severity = none
# Don't require a string format specifier.
dotnet_diagnostic.CA1305.severity = none
# Allow protected fields.
dotnet_diagnostic.CA1051.severity = none
# Don't warn about checking values that are supposedly never null. Sometimes
# they are actually null.
dotnet_diagnostic.CS8073.severity = none
# Don't remove seemingly "unnecessary" assignments, as they often have
# intended side-effects.
dotnet_diagnostic.IDE0059.severity = none
# Switch/case should always have a default clause. Tell that to Roslynator.
dotnet_diagnostic.RCS1070.severity = none
# Tell roslynator not to eat unused parameters.
dotnet_diagnostic.RCS1163.severity = none
# Tell dotnet not to remove unused parameters.
dotnet_diagnostic.IDE0060.severity = none
# Tell roslynator not to remove `partial` modifiers.
dotnet_diagnostic.RCS1043.severity = none
# Tell roslynator not to make classes static so aggressively.
dotnet_diagnostic.RCS1102.severity = none
# Roslynator wants to make properties readonly all the time, so stop it.
# The developer knows best when it comes to contract definitions with Godot.
dotnet_diagnostic.RCS1170.severity = none
# Allow expression values to go unused, even without discard variable.
# Otherwise, using Moq would be way too verbose.
dotnet_diagnostic.IDE0058.severity = none
# Don't let roslynator turn every local variable into a const.
# If we did, we'd have to specify the types of local variables far more often,
# and this style prefers type inference.
dotnet_diagnostic.RCS1118.severity = none
# Enums don't need to declare explicit values. Everyone knows they start at 0.
dotnet_diagnostic.RCS1161.severity = none
# Allow unconstrained type parameter to be checked for null.
dotnet_diagnostic.RCS1165.severity = none
# Allow keyword-based names so that parameter names like `@event` can be used.
dotnet_diagnostic.CA1716.severity = none
# Allow me to use the word Collection if I want.
dotnet_diagnostic.CA1711.severity = none
# Not disposing of objects in a test is normal within Godot because of scene tree stuff.
dotnet_diagnostic.CA1001.severity = none
# No primary constructors — not supported well by tooling.
dotnet_diagnostic.IDE0290.severity = none
# Let me comment where I like
dotnet_diagnostic.RCS1181.severity = none
# Let me write dumb if checks, keeps it readable
dotnet_diagnostic.IDE0046.severity = none
# Don't make me use expression bodies for methods
dotnet_diagnostic.IDE0022.severity = none
# Don't use collection shorhand.
dotnet_diagnostic.IDE0300.severity = none
dotnet_diagnostic.IDE0028.severity = none
dotnet_diagnostic.IDE0305.severity = none
# Don't make me populate a switch expression redundantly
dotnet_diagnostic.IDE0072.severity = none
##########################################
# License
##########################################
# The following applies as to the .editorconfig file ONLY, and is
# included below for reference, per the requirements of the license
# corresponding to this .editorconfig file.
# See: https://github.com/RehanSaeed/EditorConfig
#
# MIT License
#
# Copyright (c) 2017-2019 Muhammad Rehan Saeed
# Copyright (c) 2019 Henry Gabryjelski
#
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
# documentation files (the "Software"), to deal in the
# Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute,
# sublicense, and/or sell copies of the Software, and to permit
# persons to whom the Software is furnished to do so, subject
# to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
##########################################
# CS8981: The type name only contains lower-cased ascii characters. Such names may become reserved for the language.
dotnet_diagnostic.CS8981.severity = silent
# CS1591: Missing XML comment for publicly visible type or member
dotnet_diagnostic.CS1591.severity = silent

View File

@ -54,8 +54,8 @@ namespace lib
static public void startup( string filename ) static public void startup( string filename )
{ {
res.Mgr.register( load ); res.Mgr.Register( load );
res.Mgr.registerSub( typeof( ConfigBase ) ); res.Mgr.RegisterSub( typeof( ConfigBase ) );
s_cfg = load<ConfigCfg>( filename ); s_cfg = load<ConfigCfg>( filename );

View File

@ -1,103 +1,110 @@
 #nullable enable
using System; using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Optional; using imm; // Ensure this namespace is available
/// <summary>
/// Base context for an FSM.
namespace imm; /// MUST inherit from Recorded<TSelf> or Timed<TSelf> in your concrete class.
/// </summary>
/// <typeparam name="TSelf">The concrete Context type.</typeparam>
public abstract record class FsmContextBase<TSelf> : Recorded<TSelf>
public record class Context : imm.Recorded<Context>, Imm where TSelf : FsmContextBase<TSelf>
{ {
Meta Imm.Meta => base.Meta; // Required for 'with' expressions.
protected FsmContextBase(Recorded<TSelf> original) : base(original) { }
protected FsmContextBase() { }
} }
public record class State<TSUB, CTX>( CTX Context ) : imm.Recorded<TSUB>, Imm /// <summary>
where TSUB : State<TSUB, CTX> /// Base state for an FSM.
where CTX : Context /// MUST inherit from Recorded<TSelf> or Timed<TSelf> in your concrete class.
/// </summary>
/// <typeparam name="TSelf">The concrete State type.</typeparam>
/// <typeparam name="TCtx">The concrete Context type (must be based on FsmContextBase).</typeparam>
public abstract record class FsmStateBase<TSelf, TCtx> : Recorded<TSelf>
where TSelf : FsmStateBase<TSelf, TCtx>
where TCtx : FsmContextBase<TCtx>
{ {
Meta Imm.Meta => base.Meta; /// <summary>
/// Called when entering this state.
/// </summary>
public virtual (TCtx Context, TSelf State) OnEnter(TCtx context, FsmStateBase<TSelf, TCtx> oldState)
{
return (context, (TSelf)this);
}
virtual public (CTX, TSUB) onEnter(CTX ctx, State<TSUB, CTX> oldState) /// <summary>
{ /// Called when exiting this state.
return (ctx, (TSUB)this); /// </summary>
} public virtual (TCtx Context, TSelf State) OnExit(TCtx context, FsmStateBase<TSelf, TCtx> newState)
{
return (context, (TSelf)this);
}
virtual public (CTX, TSUB) onExit(CTX ctx, State<TSUB, CTX> newState) // Required for 'with' expressions.
{ protected FsmStateBase(Recorded<TSelf> original) : base(original) { }
return (ctx, (TSUB)this); protected FsmStateBase() { }
}
} }
/// <summary>
/// An immutable FSM base class.
public record class FSM<TSUB, ST, CTX> : imm.Recorded<TSUB>, Imm /// MUST inherit from Recorded<TSelf> or Timed<TSelf> in your concrete class.
where TSUB : FSM<TSUB, ST, CTX> /// </summary>
where ST : State<ST, CTX> /// <typeparam name="TSelf">The concrete FSM type.</typeparam>
where CTX : Context /// <typeparam name="TState">The concrete State type.</typeparam>
/// <typeparam name="TCtx">The concrete Context type.</typeparam>
public abstract record class FsmBase<TSelf, TState, TCtx> : Recorded<TSelf>
where TSelf : FsmBase<TSelf, TState, TCtx>
where TState : FsmStateBase<TState, TCtx>
where TCtx : FsmContextBase<TCtx>
{ {
Meta Imm.Meta => base.Meta; public TCtx Context { get; init; }
public TState State { get; init; }
public CTX Context { get; init; } protected FsmBase(TCtx initialContext, TState initialState)
public ST State { get; init; } {
Context = initialContext;
State = initialState;
}
public FSM( CTX context, ST stStart ) // Required for 'with' expressions.
{ protected FsmBase(Recorded<TSelf> original) : base(original)
Context = context; {
State = stStart; var o = original as FsmBase<TSelf, TState, TCtx>;
} Context = o!.Context;
State = o!.State;
}
public TSUB Transition(ST newState, string reason, /// <summary>
[CallerMemberName] string memberName = "", /// Transitions the FSM. It automatically uses the 'Process'
[CallerFilePath] string filePath = "", /// method appropriate for Recorded or Timed, thanks to virtual overrides.
[CallerLineNumber] int lineNumber = 0, /// </summary>
[CallerArgumentExpression("newState")] public TSelf Transition(
string expression = default TState newState,
) string reason,
{ [CallerMemberName] string memberName = "",
log.debug( $"Trans from {State.GetType().Name} to {newState.GetType().Name} for {reason}" ); [CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0,
[CallerArgumentExpression("newState")] string expression = "")
{
Console.WriteLine($"[FSM] Transition: {State.GetType().Name} -> {newState.GetType().Name}. Reason: {reason}");
var origState = State; var (ctxAfterExit, stateAfterExit) = State.OnExit(Context, newState);
var (ctxAfterEnter, stateAfterEnter) = newState.OnEnter(ctxAfterExit, stateAfterExit);
var (newCtx, oldState) = State.onExit(Context, newState);
var (newCTX, storeState) = newState.onEnter(newCtx, oldState);
var newFSM = this.Process( (v) => (this as TSUB) with
{
Context = newCTX,
State = storeState,
}, $"{reason}" );
return newFSM;
}
/*
public TSUB ( Func<ST, ST> fn, string reason,
[CallerMemberName] string member = "",
[CallerFilePath] string file = "",
[CallerLineNumber] int line = 0,
[CallerArgumentExpression("fn")]
string expression = default
)
{
var newState = fn( State );
if( object.ReferenceEquals( newState, State ) ) return (TSUB)this;
TSUB newFSM = this.Process( this with
{
Context = Context,
State = newState,
}, $"Processing: {newState.GetType().Name} for {reason}",
member, file, line, expression );
return (TSUB)newFSM;
}
*/
// Since 'this' is at least 'Recorded<TSelf>', we can call the
// detailed 'Process'. If 'this' is actually 'Timed<TSelf>', C#'s
// virtual dispatch will call the 'Timed' override automatically.
return Process(
fsm => (TSelf)fsm with
{
Context = ctxAfterEnter,
State = stateAfterEnter
},
$"Transition to {newState.GetType().Name}: {reason}",
memberName, filePath, lineNumber, expression
);
}
} }

View File

@ -1,404 +1,337 @@
 #nullable enable
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using lib;
namespace imm; namespace imm;
/// <summary>
/// Represents the base interface for versioned, immutable objects.
/// Provides access to metadata and potentially the previous version.
/// </summary>
public interface Obj
{
/// <summary>
/// Gets the base metadata associated with this version.
/// </summary>
Metadata_Versioned Meta { get; }
/// <summary>
/// Gets the previous version as a base object, if available.
/// Returns null if this is the first version or if history is not tracked.
/// </summary>
Obj? Old { get; }
/// <summary>
/// Creates a new version without functional change.
/// Returns the new version as an Obj.
/// </summary>
Obj Record(
string reason = "Recorded",
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0 );
}
/// <summary>
/// Obj delegate for change notifications.
/// </summary>
public delegate void ChangeDelegate<T>( T? oldVersion, T newVersion );
/// <summary>
/// Represents a generic interface for immutable objects,
/// providing access to basic processing functions and change notifications.
/// </summary>
public interface Obj<T> : Obj where T : Obj<T>
{
/// <summary>
/// Gets the change delegate associated with this object.
/// </summary>
ChangeDelegate<T> OnChange { get; set; }
/// <summary>
/// Applies a transformation and creates a new version using basic processing.
/// </summary>
T Process(
Func<T, T> fn,
string reason = "Processed",
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0 ,
[CallerArgumentExpression("fn")] string expStr = "" );
/// <summary>
/// Creates a new version without a functional change using basic processing.
/// Uses 'new' to provide a type-safe return.
/// </summary>
new T Record(
string reason = "Recorded",
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0 );
}
/* /*
T O D O : static public class ObjExtensions
T O D O : {
T O D O : /// <summary>
x) Add unit tests for all this. This will definitely benefit from them /// Creates a new version of the object with the specified reason.
/// </summary>
public static T Record<T>( this T obj, string reason = "Recorded" ) where T : Obj
{
if( obj is Recorded<T> recorded )
{
return recorded.Record( reason );
}
else if( obj is Versioned<T> versioned )
{
return versioned.Record( reason );
}
else
{
// Dont care
return obj;
}
}
}
*/ */
static public class Util // --- Metadata Hierarchy ---
public interface VersionedMeta
{ {
//This can handle both Timed and Recorded public uint Version { get; }
static public T Process<T>( ref T obj, Func<T, T> fn, string reason = "", public string Reason { get; }
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0,
[CallerArgumentExpression("fn")]
string dbgExp = "" )
where T : Recorded<T>
{
obj = obj.Process( fn, reason, dbgName, dbgPath, dbgLine, dbgExp );
return obj;
}
static public T LightProcess<T>( ref T obj, Func<T, T> fn,
string reason = "",
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0,
[CallerArgumentExpression("fn")]
string dbgExp = ""
)
where T : Versioned<T>
{
obj = obj.Process( fn, reason );
return obj;
}
}
public interface Meta
{
public uint Version => 0;
public string Reason => "";
public string Expression => "";
public string MemberName => "";
public string FilePath => "";
public int LineNumber => -1;
public DateTime CreatedAt => DateTime.MinValue;
public DateTime TouchedAt => DateTime.MinValue;
}
public interface Imm
{
public Meta Meta { get; }
public object Record( string reason = "",
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0
);
public Imm Process( Imm next,
string reason = "",
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0,
[CallerArgumentExpression("next")]
string dbgExp = ""
)
{
return next;
}
} }
//[lib.Ser( Types = lib.Types.None )] /// <summary>
public record class Versioned<T> : Imm /// Obj metadata for version tracking.
where T : Versioned<T> /// </summary>
public record Metadata_Versioned
{ {
public uint Version { get; init; } = 1;
public delegate void ChangeDelegate( T? old, T next ); public string Reason { get; init; } = "Created";
public record class MetaData : Meta
{
public uint Version { get; internal set; } = 0;
public string Reason { get; internal set; } = "";
public MetaData() { }
}
protected Versioned( )
: this( new MetaData { Version = 1, Reason = $"Versioned.cons" } )
{
}
internal Versioned( MetaData meta )
{
MetaStorage = meta;
}
virtual public T Record(
string reason = "",
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0
)
{
return Process( t => t, reason );
}
protected MetaData MetaStorage = new();
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public MetaData Meta => MetaStorage;
Meta Imm.Meta => MetaStorage;
[lib.Dont]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public ChangeDelegate OnChange = (T? old,T cur) => {};
/*
public void AddOnChange( ChangeDelegate fn,
string reason,
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0`
)
{
log.debug( $"ADD {log.whatFile(dbgPath)}({dbgLine}): {dbgName} added OnChange bcs {reason}" );
OnChange += fn;
}
public void RemOnChange( ChangeDelegate fn,
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0
)
{
log.debug( $"REM {log.whatFile(dbgPath)}({dbgLine}): {dbgName} removing OnChange" );
OnChange -= fn;
}
*/
public T Process( Func<T, T> fn, string reason = "" )
{
var newT = fn( ( T )this );
return newT with
{
MetaStorage = Meta with
{
Version = newT.Meta.Version + 1,
Reason = reason,
}
};
}
object Imm.Record( string reason, string dbgName, string dbgPath, int dbgLine ) => Record( reason, dbgName, dbgPath, dbgLine );
//public object Record( string reason = "", [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = 0 ) => Recorded( );
} }
//[lib.Ser( Types = lib.Types.None )]
public record class Recorded<T> : Versioned<T>, imm.Imm public interface RecordedMeta : VersionedMeta
where T : Recorded<T>
{ {
public string MemberName { get; }
new public record class MetaData : Versioned<T>.MetaData public string FilePath { get; }
{ public int LineNumber { get; }
[lib.Dont] public string Expression { get; }
public T? Old_backing { get; internal set; }
public T? Old => Old_backing;
public string DbgName { get; internal set; } = "";
public string DbgPath { get; internal set; } = "";
public int DbgLine { get; internal set; } = -1;
public string DbgExp { get; internal set; } = "";
public MetaData() { }
}
public Recorded() : this( new MetaData() )
{
}
public Recorded(MetaData meta) : base( meta )
{
}
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
new public MetaData Meta => MetaStorage as MetaData ?? new MetaData();
override public T Record(
string reason = "",
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0
)
{
return Process( t => t, reason, dbgName, dbgPath, dbgLine );
}
virtual public T Process( T next,
string reason = "",
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0,
[CallerArgumentExpression("next")]
string dbgExp = ""
)
{
return ProcessWork( ( old ) => next, reason, dbgName, dbgPath, dbgLine, dbgExp );
}
virtual public T Process( Func<T, T> fn,
string reason = "",
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0,
[CallerArgumentExpression("fn")]
string dbgExp = ""
)
{
return ProcessWork( fn, reason, dbgName, dbgPath, dbgLine, dbgExp );
}
virtual public T ProcessWork( Func<T, T> fn,
string reason,
string dbgName,
string dbgPath,
int dbgLine,
string dbgExp
)
{
var orig = ( T )this;
var next = fn( orig );
if( object.ReferenceEquals( orig, next) )
return next;
var ret = next with
{
//Do the Versioned code here
MetaStorage = Meta with
{
Version = orig.Meta.Version + 1,
Reason = !string.IsNullOrWhiteSpace( reason ) ? reason : next.Meta.Reason,
Old_backing = orig,
DbgName = dbgName,
DbgPath = dbgPath,
DbgLine = dbgLine,
DbgExp = dbgExp
}
};
OnChange( orig, ret );
return ret;
}
} }
public record class Timed<T> : Recorded<T>, imm.Imm
where T : Timed<T> /// <summary>
/// Metadata for version and recording (debug/caller info, history).
/// </summary>
public record Metadata_Recorded : Metadata_Versioned, RecordedMeta
{ {
internal object? OldObject { get; init; } = null;
public string MemberName { get; init; } = "";
public string FilePath { get; init; } = "";
public int LineNumber { get; init; } = 0;
public string Expression { get; init; } = "";
}
new public record class MetaData : Recorded<T>.MetaData public interface TimedMeta : RecordedMeta
{ {
public readonly DateTime CreatedAt = DateTime.Now; public DateTime CreatedAt { get; }
public DateTime TouchedAt { get; internal set; } = DateTime.Now; public DateTime TouchedAt { get; }
} }
public Timed() : this( new MetaData() )
{
}
public Timed( MetaData meta ) : base( meta ) /// <summary>
{ /// Metadata for version, recording, and timing.
} /// </summary>
public record Metadata_Timed : Metadata_Recorded, TimedMeta
{
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
public DateTime TouchedAt { get; init; } = DateTime.UtcNow;
}
[DebuggerBrowsable(DebuggerBrowsableState.Never)] // --- Record Hierarchy ---
new public MetaData Meta => MetaStorage as MetaData ?? new MetaData();
public TimeSpan Since => Meta.TouchedAt - Meta.Old?.Meta.TouchedAt ?? TimeSpan.MaxValue; /// <summary>
/// Level 1: Basic versioning. Implements Obj<T>.
/// </summary>
public record class Versioned<T> : Obj<T> where T : Versioned<T>
{
public Metadata_Versioned Meta { get; init; } = new();
public void CallOnChange() [DebuggerBrowsable( DebuggerBrowsableState.Never )]
{ public ChangeDelegate<T> OnChange { get; set; } = ( o, n ) => { };
OnChange( Meta.Old, (T)this );
}
override public T Record( public virtual Obj? Old => null;
string reason = "",
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0
)
{
return Process( t => t with { MetaStorage = t.Meta with { Reason = $"Record {reason}" }}, reason, dbgName, dbgPath, dbgLine );
}
override public T Process( T next, Metadata_Versioned Obj.Meta => this.Meta;
string reason = "", Obj? Obj.Old => this.Old;
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0,
[CallerArgumentExpression("next")]
string dbgExp = ""
)
{
return ProcessWork( ( old ) => next, reason, dbgName, dbgPath, dbgLine, dbgExp );
}
public U ProcessFn<U>( Func<U, U> fn, public Versioned() { }
string reason = "", protected Versioned( Versioned<T> original )
[CallerMemberName] string dbgName = "", {
[CallerFilePath] string dbgPath = "", OnChange = original.OnChange;
[CallerLineNumber] int dbgLine = 0, Meta = original.Meta;
[CallerArgumentExpression("fn")] }
string dbgExp = ""
)
where U : T
{
return (U)ProcessWork( fn as Func<T, T>, reason, dbgName, dbgPath, dbgLine, dbgExp );
}
override public T Process( Func<T, T> fn, public virtual T Process(
string reason = "", Func<T, T> fn,
[CallerMemberName] string dbgName = "", string reason = "Processed",
[CallerFilePath] string dbgPath = "", [CallerMemberName] string memberName = "",
[CallerLineNumber] int dbgLine = 0, [CallerFilePath] string filePath = "",
[CallerArgumentExpression("fn")] [CallerLineNumber] int lineNumber = 0 ,
string dbgExp = "" [CallerArgumentExpression("fn")] string expStr = "" )
) {
=> ProcessWork( fn, reason, dbgName, dbgPath, dbgLine, dbgExp ); var current = (T)this;
var next = fn( current );
override public T ProcessWork( Func<T, T> fn, if( ReferenceEquals( current, next ) )
string reason, return current;
string dbgName,
string dbgPath,
int dbgLine,
string dbgExp
)
{
var orig = ( T )this;
var next = fn( orig ); var newVersion = next with
{
Meta = new Metadata_Versioned { /*...*/ },
OnChange = current.OnChange
};
newVersion.OnChange( current, newVersion );
return newVersion;
}
if( object.ReferenceEquals( orig, next) ) /// <summary>
return next; /// Basic Record. Made virtual. Implements Obj<T>.Record.
/// </summary>
public virtual T Record(
string reason = "Recorded",
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0 ) => Process( t => t, reason, memberName, filePath, lineNumber );
var ret = next with /// <summary>
{ /// Implements Obj.Record by calling the virtual T Record.
MetaStorage = Meta with /// </summary>
{ Obj Obj.Record(
//Versioned string reason = "Recorded",
Version = orig.Meta.Version + 1, [CallerMemberName] string memberName = "",
Reason = !string.IsNullOrWhiteSpace( reason ) ? reason : next.Meta.Reason, [CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0 ) => this.Record( reason, memberName, filePath, lineNumber );
//Recorded
DbgName = dbgName,
DbgPath = dbgPath,
DbgLine = dbgLine,
DbgExp = dbgExp,
Old_backing = orig,
//Timed
TouchedAt = DateTime.Now,
}
};
if( OnChange != null)
OnChange( orig, ret );
return ret;
}
} }
/// <summary>
/// Level 2: Adds history and caller info.
/// </summary>
public record class Recorded<T> : Versioned<T> where T : Recorded<T>
{
new public Metadata_Recorded Meta { get; init; } = new();
new public T? Old => Meta.OldObject as T;
//public override Obj? Old => this.Old;
//Metadata_Versioned Obj.Meta => this.Meta;
public Recorded() { }
protected Recorded(Recorded<T> original) : base(original) { Meta = original.Meta; }
public virtual T Process(
Func<T, T> fn,
string reason = "",
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0,
[CallerArgumentExpression("fn")] string expStr = "")
{
var current = (T)this;
var next = fn(current);
if (ReferenceEquals(current, next)) return current;
var newMeta = new Metadata_Recorded
{
Version = current.Meta.Version + 1,
Reason = reason,
MemberName = memberName,
FilePath = filePath,
LineNumber = lineNumber,
Expression = expStr,
OldObject = current
};
var newVersion = next with { Meta = newMeta, OnChange = current.OnChange };
newVersion.OnChange(current, newVersion);
return newVersion;
}
public new T Record(
string reason = "Recorded",
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0)
{
return Process(t => t, reason, memberName, filePath, lineNumber );
}
}
/// <summary>
/// Level 3: Adds timestamps.
/// </summary>
public record class Timed<T> : Recorded<T> where T : Timed<T>
{
new public Metadata_Timed Meta { get; init; } = new();
//Metadata_Versioned Obj.Meta => this.Meta;
public TimeSpan SinceLastTouch => Meta.TouchedAt - (Old?.Meta as Metadata_Timed)?.TouchedAt ?? TimeSpan.Zero;
public TimeSpan TotalAge => Meta.TouchedAt - Meta.CreatedAt;
public Timed() { }
protected Timed(Timed<T> original) : base(original) { Meta = original.Meta; }
public override T Process(
Func<T, T> fn,
string reason = "",
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0,
[CallerArgumentExpression("fn")] string expression = "")
{
var current = (T)this;
var next = fn(current);
if (ReferenceEquals(current, next)) return current;
var newMeta = new Metadata_Timed
{
Version = current.Meta.Version + 1,
Reason = reason,
MemberName = memberName,
FilePath = filePath,
LineNumber = lineNumber,
Expression = expression,
OldObject = current,
CreatedAt = DateTime.UtcNow,
TouchedAt = DateTime.UtcNow
};
var currentTimedMeta = current.Meta;
var newVersion = next with { Meta = newMeta, OnChange = current.OnChange };
newVersion.OnChange(current, newVersion);
return newVersion;
}
public new T Record(
string reason = "Recorded",
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0)
{
return Process(t => t, reason, memberName, filePath, lineNumber );
}
}

View File

@ -1,158 +1,77 @@
using System; #nullable enable
using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Runtime.CompilerServices;
namespace imm; namespace imm;
/// <summary>
/// An immutable list implementation that tracks history, metadata, and time.
/// </summary>
public record class List<T> : Timed<List<T>>, IImmutableList<T> public record class List<T> : Timed<List<T>>, IImmutableList<T>
{ {
static public List<T> Empty { get; } = new();
ImmutableList<T> Values = ImmutableList<T>.Empty; public ImmutableList<T> Values { get; init; } = ImmutableList<T>.Empty;
public List() { }
// Required for 'with' expressions to work with the base class hierarchy
protected List(Timed<List<T>> original) : base(original) { }
// Helper to apply changes using the Process method
private List<T> Change(
Func<ImmutableList<T>, ImmutableList<T>> listChange,
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0,
[CallerArgumentExpression("listChange")] string reason = "")
{
var newValues = listChange(Values);
return ReferenceEquals(Values, newValues)
? this
: Process(l => l with { Values = newValues }, reason, memberName, filePath, lineNumber, reason);
}
public T this[int index] => (( IReadOnlyList<T> )Values)[index]; // --- IImmutableList<T> implementation using the Change helper ---
public T this[int index] => Values[index];
public int Count => Values.Count; public int Count => Values.Count;
public List<T> Add( T value )
{
return this with { Values = Values.Add( value ) };
}
public List<T> AddRange( IEnumerable<T> items )
{
return this with { Values = Values.AddRange( items ) };
}
public List<T> Clear()
{
return this with { Values = Values.Clear() };
}
public IEnumerator<T> GetEnumerator()
{
return Values.GetEnumerator();
}
public int IndexOf( T item, int index, int count, IEqualityComparer<T> equalityComparer )
{
return Values.IndexOf( item, index, count, equalityComparer );
}
public int IndexOf( T item )
{
return Values.IndexOf( item, 0, Count, EqualityComparer<T>.Default );
}
public List<T> Insert( int index, T element )
{
return this with { Values = Values.Insert( index, element ) };
}
public List<T> InsertRange( int index, IEnumerable<T> items )
{
return this with { Values = Values.InsertRange( index, items ) };
}
public int LastIndexOf( T item, int index, int count, IEqualityComparer<T> equalityComparer )
{
return Values.LastIndexOf( item, index, count, equalityComparer );
}
public List<T> Remove( T value )
{
return Remove( value, EqualityComparer<T>.Default );
}
public List<T> Remove( T value, IEqualityComparer<T> equalityComparer )
{
return this with { Values = Values.Remove( value, equalityComparer ) };
}
public List<T> RemoveAll( Predicate<T> match )
{
return this with { Values = Values.RemoveAll( match ) };
}
public List<T> RemoveAt( int index )
{
return this with { Values = Values.RemoveAt( index ) };
}
public List<T> RemoveRange( IEnumerable<T> items, IEqualityComparer<T> equalityComparer )
{
return this with { Values = Values.RemoveRange( items, equalityComparer ) };
}
public List<T> RemoveRange( int index, int count )
{
return this with { Values = Values.RemoveRange( index, count ) };
}
public List<T> Replace( T oldValue, T newValue, IEqualityComparer<T> equalityComparer )
{
return this with { Values = Values.Replace( oldValue, newValue, equalityComparer ) };
}
public List<T> SetItem( int index, T value )
{
return this with { Values = Values.SetItem( index, value ) };
}
IEnumerator IEnumerable.GetEnumerator()
{
return (( IEnumerable )Values).GetEnumerator();
}
IImmutableList<T> IImmutableList<T>.Clear()
{
return Clear();
}
IImmutableList<T> IImmutableList<T>.Add( T value )
{
return Add( value );
}
IImmutableList<T> IImmutableList<T>.AddRange( IEnumerable<T> items )
{
return AddRange( items );
}
IImmutableList<T> IImmutableList<T>.Insert( int index, T element )
{
return Insert( index, element );
}
IImmutableList<T> IImmutableList<T>.InsertRange( int index, IEnumerable<T> items )
{
return InsertRange( index, items );
}
IImmutableList<T> IImmutableList<T>.Remove( T value, IEqualityComparer<T> equalityComparer )
{
return Remove( value, equalityComparer );
}
IImmutableList<T> IImmutableList<T>.RemoveAll( Predicate<T> match )
{
return RemoveAll( match );
}
public List<T> Add(T value) => Change(v => v.Add(value));
public List<T> AddRange(IEnumerable<T> items) => Change(v => v.AddRange(items));
public List<T> Clear() => Change(v => v.Clear());
// ... Implement all other IImmutableList methods similarly ...
#region IImmutableList Implementation
public List<T> Insert( int index, T element ) => Change( v => v.Insert( index, element ) );
public List<T> InsertRange( int index, IEnumerable<T> items ) => Change( v => v.InsertRange( index, items ) );
public List<T> Remove( T value, IEqualityComparer<T>? equalityComparer ) => Change( v => v.Remove( value, equalityComparer ) );
public List<T> Remove( T value ) => Remove( value, EqualityComparer<T>.Default );
public List<T> RemoveAll( Predicate<T> match ) => Change( v => v.RemoveAll( match ) );
public List<T> RemoveAt( int index ) => Change( v => v.RemoveAt( index ) );
public List<T> RemoveRange( IEnumerable<T> items, IEqualityComparer<T>? equalityComparer ) => Change( v => v.RemoveRange( items, equalityComparer ) );
public List<T> RemoveRange( int index, int count ) => Change( v => v.RemoveRange( index, count ) );
public List<T> Replace( T oldValue, T newValue, IEqualityComparer<T>? equalityComparer ) => Change( v => v.Replace( oldValue, newValue, equalityComparer ) );
public List<T> SetItem( int index, T value ) => Change( v => v.SetItem( index, value ) );
public int IndexOf( T item, int index, int count, IEqualityComparer<T>? equalityComparer ) => Values.IndexOf( item, index, count, equalityComparer ?? EqualityComparer<T>.Default );
public int IndexOf( T item ) => IndexOf( item, 0, Count, EqualityComparer<T>.Default );
public int LastIndexOf( T item, int index, int count, IEqualityComparer<T>? equalityComparer ) => Values.LastIndexOf( item, index, count, equalityComparer ?? EqualityComparer<T>.Default );
IImmutableList<T> IImmutableList<T>.Clear() => Clear();
IImmutableList<T> IImmutableList<T>.Add( T value ) => Add( value );
IImmutableList<T> IImmutableList<T>.AddRange( IEnumerable<T> items ) => AddRange( items );
IImmutableList<T> IImmutableList<T>.Insert( int index, T element ) => Insert( index, element );
IImmutableList<T> IImmutableList<T>.InsertRange( int index, IEnumerable<T> items ) => InsertRange( index, items );
IImmutableList<T> IImmutableList<T>.Remove( T value, IEqualityComparer<T>? equalityComparer ) => Remove( value, equalityComparer );
IImmutableList<T> IImmutableList<T>.RemoveAll( Predicate<T> match ) => RemoveAll( match );
IImmutableList<T> IImmutableList<T>.RemoveAt( int index ) => RemoveAt( index ); IImmutableList<T> IImmutableList<T>.RemoveAt( int index ) => RemoveAt( index );
IImmutableList<T> IImmutableList<T>.RemoveRange( IEnumerable<T> items, IEqualityComparer<T>? equalityComparer ) => RemoveRange( items, equalityComparer );
IImmutableList<T> IImmutableList<T>.RemoveRange( IEnumerable<T> items, IEqualityComparer<T> equalityComparer )
=> RemoveRange( items, equalityComparer );
IImmutableList<T> IImmutableList<T>.RemoveRange( int index, int count ) => RemoveRange( index, count ); IImmutableList<T> IImmutableList<T>.RemoveRange( int index, int count ) => RemoveRange( index, count );
IImmutableList<T> IImmutableList<T>.Replace( T oldValue, T newValue, IEqualityComparer<T>? equalityComparer ) => Replace( oldValue, newValue, equalityComparer );
IImmutableList<T> IImmutableList<T>.Replace( T oldValue, T newValue, IEqualityComparer<T> equalityComparer )
=> Replace( oldValue, newValue, equalityComparer );
IImmutableList<T> IImmutableList<T>.SetItem( int index, T value ) => SetItem( index, value ); IImmutableList<T> IImmutableList<T>.SetItem( int index, T value ) => SetItem( index, value );
} #endregion
// --- Standard Interfaces ---
public IEnumerator<T> GetEnumerator() => Values.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)Values).GetEnumerator();
}

View File

@ -1,56 +1,47 @@
 #nullable enable
// A spot for immutable helpers
// TODO
// TODO
// TODO
// x) Wrap metadata into its own struct
// x) Make metadata a struct vs a class
using System; using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using imm;
/// <summary>
static public class iu /// Helper static class for processing immutable objects using a 'ref' pattern.
/// Provides different levels of processing based on the type.
/// </summary>
public static class iu
{ {
//This can handle both Timed and Recorded /// <summary>
static public T Process<T>( ref T obj, Func<T, T> fn, /// Processes a 'Versioned' object (Level 1).
string reason = "", /// </summary>
[CallerMemberName] string dbgName = "", public static T LightProcess<T>(
[CallerFilePath] string dbgPath = "", ref T obj,
[CallerLineNumber] int dbgLine = 0, Func<T, T> fn,
[CallerArgumentExpression("fn")] string reason = "Processed")
string dbgExpression = default ) where T : Versioned<T>
where T : imm.Recorded<T>, imm.Imm {
{ obj = obj.Process(fn, reason);
obj = obj.Process( fn, reason, dbgName, dbgPath, dbgLine, dbgExpression ); return obj;
return obj; }
}
static public imm.Timed<T> Process<T>( ref imm.Timed<T> obj, Func<T, T> fn, /// <summary>
string reason = "", /// Processes a 'Recorded' object (Level 2), capturing caller info.
[CallerMemberName] string dbgName = "", /// </summary>
[CallerFilePath] string dbgPath = "", public static T Process<T>(
[CallerLineNumber] int dbgLine = 0, ref T obj,
[CallerArgumentExpression("fn")] Func<T, T> fn,
string dbgExpression = default ) string reason = "",
where T : imm.Timed<T>, imm.Imm [CallerMemberName] string dbgName = "",
{ [CallerFilePath] string dbgPath = "",
obj = obj.Process( fn, reason, dbgName, dbgPath, dbgLine, dbgExpression ); [CallerLineNumber] int dbgLine = 0,
return obj; [CallerArgumentExpression("fn")] string dbgExpression = "")
} where T : Recorded<T> // Catches Recorded and Timed
{
// This will call the 'Timed' override if T is Timed,
// or the 'Recorded' override if T is Recorded.
obj = obj.Process(fn, reason, dbgName, dbgPath, dbgLine, dbgExpression);
return obj;
}
// No specific Process needed for Timed, as it's caught by Recorded<T>
static public T LightProcess<T>( ref T obj, Func<T, T> fn, // and its Process override handles the timing.
string reason = "",
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0,
[CallerArgumentExpression("fn")]
string dbgExpression = default )
where T : imm.Versioned<T>, imm.Imm
{
obj = obj.Process( fn, reason );
return obj;
}
} }

View File

@ -45,9 +45,9 @@ public record struct Value<T>( T _val, string _exp = "" )
[CallerArgumentExpression("v")] [CallerArgumentExpression("v")]
string dbgExp = "" string dbgExp = ""
) )
{ {
return new( v, dbgExp ); return new( v, dbgExp );
} }
} }
public struct SourceLoc public struct SourceLoc
@ -55,11 +55,11 @@ public struct SourceLoc
readonly string _reason = ""; readonly string _reason = "";
readonly string _dbgName = ""; readonly string _dbgName = "";
readonly string _dbgPath = ""; readonly string _dbgPath = "";
readonly int _dbgLine = -1; readonly int _dbgLine = -1;
public SourceLoc( string reason, string dbgName, string dbgPath, int dbgLine ) public SourceLoc( string reason, string dbgName, string dbgPath, int dbgLine )
{ {
_reason = reason; _reason = reason;
_dbgName = dbgName; _dbgName = dbgName;
_dbgPath = dbgPath; _dbgPath = dbgPath;
_dbgLine = dbgLine; _dbgLine = dbgLine;
@ -129,7 +129,7 @@ static public class log
static void StartGCWatcher() static void StartGCWatcher()
{ {
while( !s_running ) while( s_threading != ThreadState.Running )
{ {
Thread.Sleep( 10 ); Thread.Sleep( 10 );
} }
@ -140,7 +140,7 @@ static public class log
static void StartTracing() static void StartTracing()
{ {
while( !s_running ) while( s_threading != ThreadState.Running )
{ {
Thread.Sleep( 10 ); Thread.Sleep( 10 );
} }
@ -154,9 +154,9 @@ static public class log
[CallerArgumentExpression("val")] [CallerArgumentExpression("val")]
string dbgExp = "" string dbgExp = ""
) )
{ {
return new( val, dbgExp ); return new( val, dbgExp );
} }
[Flags] [Flags]
public enum LogType public enum LogType
@ -255,7 +255,7 @@ static public class log
static public void endpointForCat( string cat, Endpoints ep ) static public void endpointForCat( string cat, Endpoints ep )
{ {
ImmutableInterlocked.AddOrUpdate( ref s_logEPforCat, cat, ep, (k, v) => ep ); ImmutableInterlocked.AddOrUpdate( ref s_logEPforCat, cat, ep, ( k, v ) => ep );
} }
@ -284,7 +284,7 @@ static public class log
static int s_cwdLength = s_cwd.Length; static int s_cwdLength = s_cwd.Length;
static ImmutableDictionary<int, string> s_files = ImmutableDictionary<int, string>.Empty; static ImmutableDictionary<int, string> s_files = ImmutableDictionary<int, string>.Empty;
#region Util #region Util
static public (string, string, int) record( [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1 ) static public (string, string, int) record( [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1 )
=> (dbgName, dbgPath, dbgLine); => (dbgName, dbgPath, dbgLine);
@ -331,11 +331,11 @@ static public class log
{ {
return relativePath( path ); return relativePath( path );
} }
#endregion // Util #endregion // Util
#region Forwards #region Forwards
static public T call<T>( Func<T> func, [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerArgumentExpression("func")] string dbgExp = "" ) static public T call<T>( Func<T> func, [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerArgumentExpression( "func" )] string dbgExp = "" )
{ {
log.info( $"Calling {dbgExp}" ); log.info( $"Calling {dbgExp}" );
var val = func(); var val = func();
@ -343,53 +343,53 @@ static public class log
return val; return val;
} }
static public T var<T>( T val, [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerArgumentExpression("val")] string dbgExp = "" ) static public T var<T>( T val, [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerArgumentExpression( "val" )] string dbgExp = "" )
{ {
log.info( $"Called {dbgExp} Got: {val}" ); log.info( $"Called {dbgExp} Got: {val}" );
return val; return val;
} }
static public void call( Action func, [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerArgumentExpression("func")] string dbgExp = "" ) static public void call( Action func, [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerArgumentExpression( "func" )] string dbgExp = "" )
{ {
log.info( $"Calling {dbgExp}" ); log.info( $"Calling {dbgExp}" );
func(); func();
log.info( $"| Done" ); log.info( $"| Done" );
} }
static public void fatal( string msg, string cat = "", object? obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression("msg")] string dbgExp = "" ) static public void fatal( string msg, string cat = "", object? obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" )
{ {
logBase( msg, LogType.Fatal, path, line, member, cat, dbgExp, obj ); logBase( msg, LogType.Fatal, path, line, member, cat, dbgExp, obj );
} }
[StackTraceHidden] [StackTraceHidden]
static public void error( string msg, string cat = "", object? obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression("msg")] string dbgExp = "" ) static public void error( string msg, string cat = "", object? obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" )
{ {
logBase( msg, LogType.Error, path, line, member, cat, dbgExp, obj ); logBase( msg, LogType.Error, path, line, member, cat, dbgExp, obj );
} }
[StackTraceHidden] [StackTraceHidden]
static public void warn( string msg, string cat = "", object? obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression("msg")] string dbgExp = "" ) static public void warn( string msg, string cat = "", object? obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" )
{ {
logBase( msg, LogType.Warn, path, line, member, cat, dbgExp, obj ); logBase( msg, LogType.Warn, path, line, member, cat, dbgExp, obj );
} }
static public void high( string msg, string cat = "", object? obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression("msg")] string dbgExp = "" ) static public void high( string msg, string cat = "", object? obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" )
{ {
logBase( msg, LogType.High, path, line, member, cat, dbgExp, obj ); logBase( msg, LogType.High, path, line, member, cat, dbgExp, obj );
} }
static public void info( string msg, string cat = "", object? obj = null, static public void info( string msg, string cat = "", object? obj = null,
[CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression("msg")] string dbgExp = "" ) [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" )
{ {
logBase( msg, LogType.Info, path, line, member, cat, dbgExp, obj ); logBase( msg, LogType.Info, path, line, member, cat, dbgExp, obj );
} }
static public void debug( string msg, string cat = "", object? obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression("msg")] string dbgExp = "" ) static public void debug( string msg, string cat = "", object? obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" )
{ {
logBase( msg, LogType.Debug, path, line, member, cat, dbgExp, obj ); logBase( msg, LogType.Debug, path, line, member, cat, dbgExp, obj );
} }
static public void trace( string msg, string cat = "", object? obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression("msg")] string dbgExp = "" ) static public void trace( string msg, string cat = "", object? obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" )
{ {
logBase( msg, LogType.Trace, path, line, member, cat, dbgExp, obj ); logBase( msg, LogType.Trace, path, line, member, cat, dbgExp, obj );
} }
@ -401,19 +401,19 @@ static public class log
{ {
} }
static public void info<A>( string msg, A a, string cat = "", [CallerArgumentExpression("a")] string dbgExpA = "" ) static public void info<A>( string msg, A a, string cat = "", [CallerArgumentExpression( "a" )] string dbgExpA = "" )
{ {
} }
static public void info( string msg, object a, object b, string cat = "", [CallerArgumentExpression("a")] string dbgExpA = "", [CallerArgumentExpression("a")] string dbgExpB = "" ) static public void info( string msg, object a, object b, string cat = "", [CallerArgumentExpression( "a" )] string dbgExpA = "", [CallerArgumentExpression( "a" )] string dbgExpB = "" )
{ {
} }
#endregion #endregion
#region Helpers #region Helpers
static public void logProps( object obj, string header, LogType type = LogType.Debug, string cat = "", string prefix = "", [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression("obj")] string dbgExpObj = "" ) static public void logProps( object obj, string header, LogType type = LogType.Debug, string cat = "", string prefix = "", [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression( "obj" )] string dbgExpObj = "" )
{ {
var list = refl.GetAllProperties( obj.GetType() ); var list = refl.GetAllProperties( obj.GetType() );
@ -434,7 +434,7 @@ static public class log
} }
catch( Exception ex ) catch( Exception ex )
{ {
logBase( $"Exception processing {pi.Name} {ex.Message}", LogType.Error, path, line, member, cat, dbgExpObj, obj); logBase( $"Exception processing {pi.Name} {ex.Message}", LogType.Error, path, line, member, cat, dbgExpObj, obj );
} }
} }
@ -454,7 +454,7 @@ static public class log
log.warn( $"Got {notExpectedValue} instead of {value}{falseString}" ); log.warn( $"Got {notExpectedValue} instead of {value}{falseString}" );
} }
} }
#endregion #endregion
static object s_lock = new object(); static object s_lock = new object();
@ -516,41 +516,41 @@ static public class log
lock( s_lock ) lock( s_lock )
{ {
//We're already running, so just tell folks, and jump back //We're already running, so just tell folks, and jump back
if( s_running ) if( s_threading == ThreadState.Running )
{ {
log.info( $"Already running, so this is a NOP" ); log.info( $"Already running, so this is a NOP" );
} }
s_running = true; s_threading = ThreadState.Running;
s_startTime = DateTime.Now; s_startTime = DateTime.Now;
s_cwd = Directory.GetCurrentDirectory(); s_cwd = Directory.GetCurrentDirectory();
s_cwdLength = s_cwd.Length; s_cwdLength = s_cwd.Length;
s_endpoints = endpoints; s_endpoints = endpoints;
var dir = Path.GetDirectoryName( filename ); var dir = Path.GetDirectoryName( filename );
if( dir?.Length > 0 ) if( dir?.Length > 0 )
{ {
Directory.CreateDirectory( dir ); Directory.CreateDirectory( dir );
} }
s_stream = new FileStream( filename, FileMode.Append, FileAccess.Write ); s_stream = new FileStream( filename, FileMode.Append, FileAccess.Write );
s_writer = new StreamWriter( s_stream, Encoding.UTF8, 128, true ); s_writer = new StreamWriter( s_stream, Encoding.UTF8, 128, true );
//s_errorStream = new FileStream( filename + ".error", FileMode.Append, FileAccess.Write ); //s_errorStream = new FileStream( filename + ".error", FileMode.Append, FileAccess.Write );
//s_errorWriter = new StreamWriter( s_errorStream ); //s_errorWriter = new StreamWriter( s_errorStream );
{ {
var time = DateTime.Now; var time = DateTime.Now;
// Header for this run // Header for this run
var blankLine = new LogEvent( LogType.Raw, $"", "", 0, "", "lib.time", "", null ); var blankLine = new LogEvent( LogType.Raw, $"", "", 0, "", "lib.time", "", null );
var beginLine = new LogEvent( LogType.Raw, $"Begin B E G I N ******************************************************************************************************************", "", 0, "", "lib.time", "", null ); var beginLine = new LogEvent( LogType.Raw, $"Begin B E G I N ******************************************************************************************************************", "", 0, "", "lib.time", "", null );
var timeLine = new LogEvent( LogType.Raw, $"D A T E {time.Year}/{time.Month.ToString("00")}/{time.Day.ToString("00")} T I M E {time.Hour.ToString("00")}:{time.Minute.ToString("00")}:{time.Second.ToString("00")}.{time.Millisecond.ToString("000")}{time.Microsecond.ToString("000")}", "", 0, "", "lib.time", "", null ); var timeLine = new LogEvent( LogType.Raw, $"D A T E {time.Year}/{time.Month.ToString( "00" )}/{time.Day.ToString( "00" )} T I M E {time.Hour.ToString( "00" )}:{time.Minute.ToString( "00" )}:{time.Second.ToString( "00" )}.{time.Millisecond.ToString( "000" )}{time.Microsecond.ToString( "000" )}", "", 0, "", "lib.time", "", null );
//writeToAll( endLine ); //writeToAll( endLine );
@ -561,61 +561,88 @@ static public class log
writeToAll( timeLine ); writeToAll( timeLine );
writeToAll( blankLine ); writeToAll( blankLine );
}
LogEvent msgStartupBegin = new LogEvent( LogType.Info, $"startup BEGIN", "", 0, "", "log.startup", "", null );
writeToAll( msgStartupBegin );
LogEvent msgFilename = new LogEvent( LogType.Info, $"Logging in {filename}", "", 0, "", "log.startup", "", null );
writeToAll( msgFilename );
StartThread();
//info( $"Logging in {filename}" );
LogGC.RegisterObjectId( s_lock );
//Debug.Listeners.Add( this );
//var evt = CreateLogEvent( LogType.Info, $"startup", "System", null );
//s_events.Enqueue( evt );
/*
if( (endpoints & Endpoints.Console) == Endpoints.Console )
{
addDelegate(WriteToConsole);
}
*/
//LogEvent msgStartupBegin = new LogEvent( LogType.Info, $"startup END", "", 0, "", "lib.time", "", null );
//writeToAll( msgStartupBegin );
info( $"startup END", cat: "log.startup" );
} }
LogEvent msgStartupBegin = new LogEvent( LogType.Info, $"startup BEGIN", "", 0, "", "log.startup", "", null ); }
writeToAll( msgStartupBegin );
LogEvent msgFilename = new LogEvent( LogType.Info, $"Logging in {filename}", "", 0, "", "log.startup", "", null ); private static void StartThread()
writeToAll( msgFilename ); {
var start = new ThreadStart( threadLoop );
var start = new ThreadStart( run );
s_thread = new Thread( start ); s_thread = new Thread( start );
s_thread.Priority = ThreadPriority.BelowNormal; s_thread.Priority = ThreadPriority.BelowNormal;
s_thread.Name = $"Logging"; s_thread.Name = $"Logging";
s_thread.Start(); s_thread.Start();
//info( $"Logging in {filename}" );
LogGC.RegisterObjectId( s_lock );
//Debug.Listeners.Add( this );
//var evt = CreateLogEvent( LogType.Info, $"startup", "System", null );
//s_events.Enqueue( evt );
/*
if( (endpoints & Endpoints.Console) == Endpoints.Console )
{
addDelegate(WriteToConsole);
}
*/
//LogEvent msgStartupBegin = new LogEvent( LogType.Info, $"startup END", "", 0, "", "lib.time", "", null );
//writeToAll( msgStartupBegin );
info( $"startup END", cat: "log.startup" );
}
} }
static bool s_running = false; private static void StopThread( ThreadState thread )
static void run()
{ {
while( !s_running ) log.info( $"Setting thread to {thread}");
s_threading = thread;
int count = 0;
while( s_thread.IsAlive )
{
Thread.Sleep( 0 );
count++;
}
Console.WriteLine( $"Waited {count} loops" );
}
public enum ThreadState
{
Invalid,
Running,
Paused,
Finished,
}
static ThreadState s_threading = ThreadState.Invalid;
static void threadLoop()
{
while( s_threading != ThreadState.Running )
{ {
// TODO PERF Replace this with a semaphore/mutex // TODO PERF Replace this with a semaphore/mutex
Thread.Sleep( 1 ); Thread.Sleep( 1 );
} }
while( s_running ) while( s_threading != ThreadState.Running )
{ {
while( s_events.TryDequeue( out var evt ) ) while( s_events.TryDequeue( out var evt ) )
{ {
@ -626,22 +653,18 @@ static public class log
Thread.Sleep( 1 ); Thread.Sleep( 1 );
} }
var endLine = new LogEvent( LogType.Raw, $"End E N D ******************************************************************************************************************", "", 0, "", "lib.time", "", null ); //var endLine = new LogEvent( LogType.Raw, $"End E N D ******************************************************************************************************************", "", 0, "", "lib.time", "", null );
writeToAll( endLine ); var endLine = new LogEvent( LogType.Raw, $"Thread state {s_threading} ******************************************************************************************************************", "", 0, "", "lib.time", "", null );
writeToAll( endLine );
} }
public static void stop() public static void stop()
{ {
if( !s_running ) return; if( s_threading == ThreadState.Finished )
return;
s_running = false; StopThread( ThreadState.Finished );
while( s_thread.IsAlive )
{
Thread.Sleep( 0 );
}
s_writer?.Close(); s_writer?.Close();
s_stream?.Close(); s_stream?.Close();
@ -651,6 +674,24 @@ static public class log
} }
public static void pauseThread()
{
log.info("Pausing thread" );
s_threading = ThreadState.Paused;
StopThread( ThreadState.Paused );
}
public static void unpauseThread()
{
s_threading = ThreadState.Running;
StartThread();
}
static public void addDelegate( Log_delegate cb ) static public void addDelegate( Log_delegate cb )
{ {
s_delegates.Add( cb ); s_delegates.Add( cb );
@ -660,14 +701,22 @@ static public class log
{ {
switch( type ) switch( type )
{ {
case LogType.Trace: return ' '; case LogType.Trace:
case LogType.Debug: return ' '; return ' ';
case LogType.Info: return ' '; case LogType.Debug:
case LogType.High: return '+'; return ' ';
case LogType.Warn: return '+'; case LogType.Info:
case LogType.Error: return '*'; return ' ';
case LogType.Fatal: return '*'; case LogType.High:
default: return '?'; return '+';
case LogType.Warn:
return '+';
case LogType.Error:
return '*';
case LogType.Fatal:
return '*';
default:
return '?';
} }
} }
@ -725,7 +774,7 @@ static public class log
var truncatedCat = evt.Cat.Substring( 0, Math.Min( s_catWidth, evt.Cat.Length ) ); var truncatedCat = evt.Cat.Substring( 0, Math.Min( s_catWidth, evt.Cat.Length ) );
var timeHdr = $"{s_timeHeader}{((int)span.TotalMinutes).ToString("000")}:{span.Seconds.ToString("D2")}.{span.Milliseconds.ToString("000")}"; var timeHdr = $"{s_timeHeader}{( (int)span.TotalMinutes ).ToString( "000" )}:{span.Seconds.ToString( "D2" )}.{span.Milliseconds.ToString( "000" )}";
var msgHdr = string.Format( $"{timeHdr} | {{0,-{s_catWidth}}}{{1}}| ", truncatedCat, sym ); var msgHdr = string.Format( $"{timeHdr} | {{0,-{s_catWidth}}}{{1}}| ", truncatedCat, sym );
@ -750,17 +799,20 @@ static public class log
var truncatedCat = evt.Cat.Substring( 0, Math.Min( s_catWidth, evt.Cat.Length ) ); var truncatedCat = evt.Cat.Substring( 0, Math.Min( s_catWidth, evt.Cat.Length ) );
if( string.IsNullOrWhiteSpace( truncatedCat) ) truncatedCat = $"B R O K E N truncatedCat"; if( string.IsNullOrWhiteSpace( truncatedCat ) )
truncatedCat = $"B R O K E N truncatedCat";
//Dont really need the year-month-day frankly. //Dont really need the year-month-day frankly.
//var timeHdr = $"{evt.Time.Year}-{evt.Time.Month.ToString("00")}-{evt.Time.Day.ToString("00")} {evt.Time.Hour.ToString("00")}:{evt.Time.Minute.ToString("00")}:{evt.Time.Second.ToString("00")}.{evt.Time.Millisecond.ToString("000")}{evt.Time.Microsecond.ToString("000")}"; //var timeHdr = $"{evt.Time.Year}-{evt.Time.Month.ToString("00")}-{evt.Time.Day.ToString("00")} {evt.Time.Hour.ToString("00")}:{evt.Time.Minute.ToString("00")}:{evt.Time.Second.ToString("00")}.{evt.Time.Millisecond.ToString("000")}{evt.Time.Microsecond.ToString("000")}";
var timeHdr = $"{evt.Time.Hour.ToString("00")}:{evt.Time.Minute.ToString("00")}:{evt.Time.Second.ToString("00")}.{evt.Time.Millisecond.ToString("000")}{evt.Time.Microsecond.ToString("000")}"; var timeHdr = $"{evt.Time.Hour.ToString( "00" )}:{evt.Time.Minute.ToString( "00" )}:{evt.Time.Second.ToString( "00" )}.{evt.Time.Millisecond.ToString( "000" )}{evt.Time.Microsecond.ToString( "000" )}";
if( string.IsNullOrWhiteSpace( timeHdr) ) timeHdr = $"B R O K E N timeHdr"; if( string.IsNullOrWhiteSpace( timeHdr ) )
timeHdr = $"B R O K E N timeHdr";
var msgHdr = string.Format( $"{timeHdr} | {{0,-{s_catWidth}}}{{1}}| ", truncatedCat, sym ); var msgHdr = string.Format( $"{timeHdr} | {{0,-{s_catWidth}}}{{1}}| ", truncatedCat, sym );
if( string.IsNullOrWhiteSpace( msgHdr) ) msgHdr = $"B R O K E N msgHdr"; if( string.IsNullOrWhiteSpace( msgHdr ) )
msgHdr = $"B R O K E N msgHdr";
return msgHdr; return msgHdr;
@ -793,7 +845,7 @@ static public class log
var msg = evt.Msg; var msg = evt.Msg;
if( (string.IsNullOrWhiteSpace(msg) && evt.LogType != LogType.Raw) || msg.Contains( (char)0 ) ) if( ( string.IsNullOrWhiteSpace( msg ) && evt.LogType != LogType.Raw ) || msg.Contains( (char)0 ) )
{ {
msg = "B R O K E N msg"; msg = "B R O K E N msg";
} }
@ -831,7 +883,7 @@ static public class log
{ {
s_lastDisplaySeconds = curSeconds; s_lastDisplaySeconds = curSeconds;
var minuteEvt = new LogEvent( LogType.Raw, $"T I M E ==> {evt.Time.Hour.ToString("00")}:{evt.Time.Minute.ToString("00")}:{evt.Time.Second.ToString("00")}.{evt.Time.Millisecond.ToString("000")} : {evt.Time.ToShortDateString()}", "", 0, "", "lib.time", "", null ); var minuteEvt = new LogEvent( LogType.Raw, $"T I M E ==> {evt.Time.Hour.ToString( "00" )}:{evt.Time.Minute.ToString( "00" )}:{evt.Time.Second.ToString( "00" )}.{evt.Time.Millisecond.ToString( "000" )} : {evt.Time.ToShortDateString()}", "", 0, "", "lib.time", "", null );
minuteEvt.Time = evt.Time; minuteEvt.Time = evt.Time;
writeSpecialEvent( minuteEvt ); writeSpecialEvent( minuteEvt );
} }
@ -893,7 +945,7 @@ static public class log
} }
catch( Exception ex ) catch( Exception ex )
{ {
#region Catch #region Catch
Console.WriteLine( "EXCEPTION DURING LOGGING" ); Console.WriteLine( "EXCEPTION DURING LOGGING" );
Console.WriteLine( "EXCEPTION DURING LOGGING" ); Console.WriteLine( "EXCEPTION DURING LOGGING" );
Console.WriteLine( "EXCEPTION DURING LOGGING" ); Console.WriteLine( "EXCEPTION DURING LOGGING" );
@ -907,7 +959,7 @@ static public class log
Debug.WriteLine( "EXCEPTION DURING LOGGING" ); Debug.WriteLine( "EXCEPTION DURING LOGGING" );
Debug.WriteLine( "EXCEPTION DURING LOGGING" ); Debug.WriteLine( "EXCEPTION DURING LOGGING" );
Debug.WriteLine( $"Exception {ex}" ); Debug.WriteLine( $"Exception {ex}" );
#endregion #endregion
} }
} }

View File

@ -1,444 +1,434 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Text;
//using System.Threading.Tasks;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Reflection; using System.Reflection;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Threading;
using System.IO;
using Microsoft.CodeAnalysis;
using System.Runtime.CompilerServices; 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 #nullable enable
namespace res; namespace res;
// A delegate representing a function that can load a resource of type T.
public delegate T Load<out T>( string filename );
using ImmDefLoad = ImmutableQueue<(string name, Ref)>; /// <summary>
/// Abstract base class for a resource reference.
public interface Res_old /// 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() { }
} }
[DebuggerDisplay("Path = {path}")] /// <summary>
abstract public class Ref : lib.I_Serialize /// A typed reference to a resource of type T.
{ /// Handles lazy loading and access to the resource.
static public bool s_verboseLogging = false; /// </summary>
public string Filename =>path;
public Ref( string filename = "{empty_filename}",
string reason = "",
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0
)
{
path = filename;
if( s_verboseLogging ) log.info( $"Ref: {GetType().Name} {path}" );
_reason = reason;
_dbgName = dbgName;
_dbgPath = dbgPath;
_dbgLine = dbgLine;
}
abstract public object lookup(
string reason = "",
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0
);
virtual public void OnChange()
{
}
virtual internal void load()
{
}
private string path = "{set_from_inline_cons}";
protected string _reason = "";
protected string _dbgName = "";
protected string _dbgPath = "";
protected int _dbgLine = 0;
protected string _dbgExp = "";
}
[Serializable] [Serializable]
[DebuggerDisplay("Path = {path} / Res = {res}")] [DebuggerDisplay( "Path = {Filename} / Res = {m_res}" )]
public class Ref<T> : Ref where T : class, new() public class Ref<T> : Ref where T : class, new()
{ {
public T res => m_res != null ? m_res : lookup(); [NonSerialized]
private T? m_res;
override public T lookup( /// <summary>
string reason = "", /// Gets the resource, loading it if necessary.
[CallerMemberName] string dbgName = "", /// </summary>
[CallerFilePath] string dbgPath = "", public T Res => m_res ?? Lookup();
[CallerLineNumber] int dbgLine = 0
) /// <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 )
{ {
m_res = Mgr.load<T>( Filename, $"Ref lookup", dbgName, dbgPath, dbgLine ); if( VerboseLogging )
if( s_verboseLogging ) log.info( $"Ref.lookup {GetType().Name} {GetType().GenericTypeArguments[0]} path {Filename}" ); log.info( $"Ref<T> Created: {GetType().Name}<{typeof( T ).Name}> {Filename}" );
return m_res;
}
//For serialization
/*
public Ref()
:
base( "{set_from_ref<>_default_cons}" )
{
if( s_verboseLogging ) log.info( $"Ref {GetType().Name} {GetType().GenericTypeArguments[0]} path {Filename}" );
}
*/
public Ref( string filename = "",
string reason = "",
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0
)
:
base( !string.IsNullOrWhiteSpace(filename) ? filename : $"{{{dbgName}_{log.whatFile(dbgPath)}}}" , reason, dbgName, dbgPath, dbgLine )
{
if( s_verboseLogging ) log.info( $"Ref {GetType().Name} {GetType().GenericTypeArguments[0]} path {Filename}" );
m_res = default; m_res = default;
} }
/// <summary>
override internal void load() /// Looks up and loads the resource.
/// </summary>
public override T Lookup(
string reason = "",
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0 )
{ {
m_res = Mgr.load<T>( Filename, _reason, _dbgName, _dbgPath, _dbgLine ); // If already loaded, return it.
if( s_verboseLogging ) log.info( $"Ref.load {GetType().Name} {GetType().GenericTypeArguments[0]} path {Filename}" ); 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;
} }
public object OnDeserialize( object enclosing ) /*
/// <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 enclosing; 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}" );
}
} }
static public Ref<T> createAsset( T v, string path, /// <summary>
string reason = "", /// Creates a new resource asset and saves it, returning a Ref to it.
[CallerMemberName] string dbgName = "", /// Handles existing files by renaming them.
[CallerFilePath] string dbgPath = "", /// </summary>
[CallerLineNumber] int dbgLine = 0 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 ) ) if( File.Exists( path ) )
{ {
log.warn( $"For {typeof(T).Name}, saving asset to {path}, but it already exists" ); log.warn( $"Asset exists: {path}. Renaming before creating." );
var newPath = $"{path}_{DateTime.Now:yyyyMMdd_HHmmss}";
var newPath = $"{path}_{DateTime.Now.ToShortDateString()}_{DateTime.Now.ToShortTimeString()}"; File.Move( path, newPath );
log.warn( $"Renamed existing asset to: {newPath}" );
System.IO.File.Move(path, newPath );
log.warn( $"For {typeof(T).Name}, renamed to {newPath}" );
} }
var createReason = $"Type {v.GetType().Name}"; // 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}." );
if( v is imm.Imm imm ) // 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.
createReason = $"{imm?.Meta}"; // Let's assume you'll add saving logic here.
} // Mgr.Save(value, path); // Example: Needs implementation
var newRef = new Ref<T>( path, $"createAsset {createReason} bcs {reason}", dbgName, dbgPath, dbgLine ); 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; return newRef;
} }
[NonSerialized]
protected T? m_res;
} }
public class Resource /// <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
{ {
static public Mgr mgr = new(); // Internal holder for type-specific loaders.
} private abstract class LoadHolder { public abstract object Load( string filename ); }
private class LoadHolder<T> : LoadHolder
public delegate T Load<out T>( string filename );
abstract class LoadHolder
{
public abstract object load();
}
class LoadHolder<T> : LoadHolder
{
public LoadHolder( Load<T> fnLoad )
{ {
_fnLoad = fnLoad; private readonly Load<T> _fnLoad;
public LoadHolder( Load<T> fnLoad ) { _fnLoad = fnLoad; }
public override object Load( string filename ) => _fnLoad( filename )!;
} }
public Load<T> _fnLoad; // Cache for resource holders (contains WeakReferences).
private static class ResCache<T> where T : class, new()
public override object load()
{
return load();
}
}
public record class ResourceHolder<T>( WeakReference<T> weak, string Name, DateTime captured ) : imm.Recorded<ResourceHolder<T>>
where T : class
{
}
//generic classes make a new static per generic type
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;
}
public class Mgr
{
static public void startup()
{ {
public static T s_default = new();
public static ImmutableDictionary<string, ResourceHolder<T>> s_cache = ImmutableDictionary<string, ResourceHolder<T>>.Empty;
} }
static public void register<T>( Load<T> loader ) 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()
{ {
Debug.Assert( !Resource.mgr.m_loaders.ContainsKey( typeof( T ) ) ); var type = typeof( T );
if( s_loaders.ContainsKey( type ) )
var lh = new LoadHolder<T>( loader );
ImmutableInterlocked.TryAdd( ref Resource.mgr.m_loaders, typeof( T ), lh );
}
//Register all subclasses of a particular type
//???? Should we just always do this?
static public void registerSub<T>()
{
registerSub( typeof(T) );
}
static public void registerSub( Type baseType )
{
log.info( $"Registering loader for {baseType.Name}" );
Type[] typeParams = new Type[1];
foreach( var mi in baseType.GetMethods() )
{ {
if( mi.Name == "res_load" && mi.IsGenericMethod ) 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 ass in AppDomain.CurrentDomain.GetAssemblies() ) foreach( var t in ass.GetTypes() )
{ {
foreach( var t in ass.GetTypes() ) if( t.IsClass && !t.IsAbstract && baseType.IsAssignableFrom( t ) && t != baseType )
{ {
if( !baseType.IsAssignableFrom( t ) ) try
continue; {
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 );
log.debug( $"Making a loader for {t.Name}" ); var loadHolderType = typeof( LoadHolder<> ).MakeGenericType( t );
var loadHolder = Activator.CreateInstance( loadHolderType, loaderDelegate ) as LoadHolder;
typeParams[0] = t; if( loadHolder != null )
var mi_ng = mi.MakeGenericMethod( typeParams ); {
if( !ImmutableInterlocked.TryAdd( ref s_loaders, t, loadHolder ) )
var loadGenType = typeof( Load<> ); {
log.debug( $"Loader for {t.Name} already exists, skipping." );
var loadType = loadGenType.MakeGenericType( t ); }
else
var loader = Delegate.CreateDelegate( loadType, mi_ng ); {
log.info( $"Registered loader for subtype {t.Name}." );
var lhGenType = typeof( LoadHolder<> ); }
}
var lhType = lhGenType.MakeGenericType( t ); }
catch( Exception ex )
var lh = Activator.CreateInstance( lhType, loader ) as LoadHolder; {
log.error( $"Failed to create loader for subtype {t.Name}: {ex.Message}" );
ImmutableInterlocked.TryAdd( ref Resource.mgr.m_loaders!, t, lh ); }
} }
} }
return; }
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}" );
} }
} }
} }
static public Ref<T> lookup<T>( string filename, /// <summary>
string reason = "", /// Creates a Ref<T> for a given filename.
[CallerMemberName] string dbgName = "", /// </summary>
[CallerFilePath] string dbgPath = "", public static Ref<T> Lookup<T>(
[CallerLineNumber] int dbgLine = 0 string filename,
) where T : class, new() 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 ); return new Ref<T>( filename, reason, dbgName, dbgPath, dbgLine );
} }
/* /// <summary>
/// Loads a resource, handling caching and thread-safe loading.
static public Ref lookup( string filename, Type t, /// </summary>
string reason = "", public static T Load<T>(
[CallerMemberName] string dbgName = "", string filename,
[CallerFilePath] string dbgPath = "", string reason = "",
[CallerLineNumber] int dbgLine = 0 [CallerMemberName] string dbgName = "",
) [CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0 ) where T : class, new()
{ {
return new Ref( filename, reason, dbgName, dbgPath, dbgLine ); // 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;
}
} }
// @@@ TODO Pass information through here /// <summary>
static public T? load<T>( string filename ) where T : class /// 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( ResCache<T>.s_cache.TryGetValue( filename, out var holder ) )
{ {
if( holder.WeakRef.TryGetTarget( out var v ) )
if( holder.weak.TryGetTarget( out var v ) )
{ {
return v; value = v;
return true;
} }
log.info( $"{filename} was in cache but dropped (GC'd), needs reloading." );
log.info( $"{filename} was in cache, but its been dropped, reloading." );
} }
value = default!;
log.warn( $"Block Loading {filename}." ); return false;
var newV = actualLoad<T>( filename );
return newV;
}
*/
static public T load<T>( string filename,
string reason = "",
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0
) where T : class, new()
{
if( ResCache<T>.s_cache.TryGetValue( filename, out var holder ) )
{
if( holder.weak.TryGetTarget( out var v ) )
{
return v;
}
log.info( $"{filename} was in cache, but its been dropped, reloading." );
}
log.warn( $"Block Loading {filename}." );
var newV = actualLoad<T>( filename );
if( newV is imm.Imm imm )
{
newV = (T)imm.Record( $"Loading because {reason}", dbgName, dbgPath, dbgLine );
}
return newV;
} }
static public T actualLoad<T>( string filename ) where T : class, new() /// <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()
{ {
lock(s_loadingLock) if( s_loaders.TryGetValue( typeof( T ), out var loaderHolder ) )
{ {
if( s_loading.TryGetValue( filename, out var evt ) ) var loadedObject = loaderHolder.Load( filename );
if( loadedObject is T value )
{ {
if( ResCache<T>.s_cache.TryGetValue( filename, out var holder ) ) var meta = (value as imm.Obj)?.Meta;
// If it's an immutable object, record its loading.
if( value is imm.Obj imm )
{ {
if( holder.weak.TryGetTarget( out var v ) ) return (T)imm.Record( $"Loading bcs {reason}", dbgName, dbgPath, dbgLine );
{
log.trace( $"{typeof(T).Name} loading {filename}" );
return v;
}
log.error( $"{filename} was in cache, but its been dropped, reloading." );
} }
return value;
} }
else
{ {
if( Resource.mgr.m_loaders.TryGetValue( typeof( T ), out var loaderGen ) ) log.error( $"Loader for {typeof( T ).Name} returned wrong type: {loadedObject?.GetType().Name}" );
{ return ResCache<T>.s_default;
var loader = loaderGen as LoadHolder<T>;
var v = loader!._fnLoad( filename );
var weak = new WeakReference<T>( v );
var holder = new ResourceHolder<T>( weak, $"", DateTime.Now ).Record();
log.info( $"To {typeof(T).Name} add {filename}" );
var alreadyAdded = !ImmutableInterlocked.TryAdd( ref ResCache<T>.s_cache, filename, holder );
if( alreadyAdded )
{
log.error( $"Key {filename} already existed, though it shouldnt." );
}
return v;
}
else
{
log.error( $"Loader could not be found for type {typeof( T )}" );
return ResCache<T>.s_default;
}
} }
} }
else
//return actualLoad<T>( filename );
}
static object s_loadingLock = new object();
static ImmutableDictionary<string, AutoResetEvent> s_loading = ImmutableDictionary<string, AutoResetEvent>.Empty;
//static ImmDefLoad s_deferredLoad = ImmDefLoad.Empty;
public Mgr()
{
log.info( $"Creating Res.Mgr" );
/*
var ts = new ThreadStart( deferredLoader );
m_deferredLoader = new Thread( ts );
*/
LogGC.RegisterObjectId( s_loadingLock );
//m_deferredLoader.Start();
}
/*
void deferredLoader()
{
while( true )
{ {
Thread.Sleep( 1 ); log.error( $"Loader could not be found for type {typeof( T ).Name} for file {filename}" );
return ResCache<T>.s_default;
if( ImmutableInterlocked.TryDequeue( ref s_deferredLoad, out var v ) )
{
v.Item2.load();
}
} }
} }
//*/
ImmutableDictionary<Type, LoadHolder> m_loaders = ImmutableDictionary<Type, LoadHolder>.Empty;
//Thread m_deferredLoader;
} }

View File

@ -17,7 +17,7 @@ using System.Reflection;
namespace lib namespace lib
{ {
//Old, use 2 now. //Old, use 2 now.
class XmlFormatter: IFormatter class XmlFormatter_BAD: IFormatter
{ {
StreamingContext m_context; StreamingContext m_context;
//SerializationMode m_mode; //SerializationMode m_mode;

View File

@ -45,6 +45,7 @@ namespace lib
object OnDeserialize( object enclosing ) => this; object OnDeserialize( object enclosing ) => this;
} }
[Flags] [Flags]
public enum Types public enum Types
{ {
@ -543,7 +544,7 @@ namespace lib
{ {
if( _cfg.VerboseLogging ) log.info( $"" ); if( _cfg.VerboseLogging ) log.info( $"" );
var isImm = typeof(imm.Imm).IsAssignableFrom( narrowType ); var isImm = typeof(imm.Obj).IsAssignableFrom( narrowType );
XmlNodeList allChildren = elem.ChildNodes; XmlNodeList allChildren = elem.ChildNodes;
@ -701,7 +702,7 @@ namespace lib
} }
else else
{ {
var imm = obj as imm.Imm; var imm = obj as imm.Obj;
var newObj = imm.Record( $"From XML {fromStr}:{elem.ParentNode?.Name}{elem.Name}" ); var newObj = imm.Record( $"From XML {fromStr}:{elem.ParentNode?.Name}{elem.Name}" );
return newObj; return newObj;
} }
@ -1343,7 +1344,7 @@ namespace lib
HashSet<string> whitelistFields, whitelistProps; HashSet<string> whitelistFields, whitelistProps;
GetFilters( _cfg.TypesDefault, mi, narrowType, out filterFields, out filterProps, out doImpls, out doFields, out doProps, out whitelistFields, out whitelistProps ); GetFilters( _cfg.TypesDefault, mi, narrowType, out filterFields, out filterProps, out doImpls, out doFields, out doProps, out whitelistFields, out whitelistProps );
var isImm = typeof(imm.Imm).IsAssignableFrom( narrowType ); var isImm = typeof(imm.Obj).IsAssignableFrom( narrowType );
if( doFields || doImpls ) if( doFields || doImpls )
{ {

917
ser/XmlSer.cs Normal file
View File

@ -0,0 +1,917 @@
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 serdes;
#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> ser, Func<string, string, object> des );
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 ser, XmlWriter writer, object? obj, string name, Type memberType, bool forceType );
object? ReadXml( XmlSer ser, 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 ser, XmlElement elem, Type expectedType, object? existing )
{
string val = elem.HasAttribute( "v" ) ? elem.GetAttribute( "v" ) : elem.InnerText;
if( val == "null" )
return null;
return ser._resolver.ConvertSimple( val, expectedType );
}
public void WriteXml( XmlSer ser, 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 = ser._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 || ser._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 ser, XmlElement elem, Type expectedType, object? existing )
{
var ti = ser._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.des( expectedType.FullName, proxyVal );
}
public void WriteXml( XmlSer ser, XmlWriter writer, object? obj, string name, Type memberType, bool forceType )
{
if( obj == null )
{ ser.GetHandler( typeof( object ) ).WriteXml( ser, writer, null, name, memberType, forceType ); return; }
var ti = ser._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.ser( 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 ser, XmlElement elem, Type expectedType, object? existing )
{
// Create/Get instance (needs FormatterServices for ISerializable)
object obj = existing ?? FormatterServices.GetUninitializedObject( expectedType );
long id = ser.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 = ser._resolver.FindType( childElem.GetAttribute( "_.t" ) );
if( childType != null )
{
var desValue = ser.ReadNode( childElem, childType, null );
serInfo.AddValue( childName, desValue, childType );
}
}
}
var context = new StreamingContext( StreamingContextStates.All ); // Or use ser.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 ser, 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 );
ser.WriteTypeAttr( writer, memberType, obj.GetType() );
if( ser.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 )
{
ser.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 ser, 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 )!;
ser.TrackIfGraph( list, elem ); // Track list if graph
// Populate the list
foreach( XmlNode node in elem.ChildNodes )
{
if( node is XmlElement childElem )
{
list.Add( ser.ReadNode( childElem, elemType, null ) );
}
}
// Convert to the final expected type (Array, Immutable*, List)
return ConvertToFinalCollection( list, expectedType, elemType );
}
public void WriteXml( XmlSer ser, 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 );
ser.WriteTypeAttr( writer, memberType, obj.GetType() );
if( ser.HandleGraphWrite( writer, obj, out bool first ) )
{
if( first )
{
Type elemType = GetElementType( obj.GetType() );
int i = 0;
foreach( var item in collection )
{
ser.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 ser, XmlElement elem, Type expectedType, object? existing )
{
var actualType = ser._resolver.Resolve( elem, expectedType );
var ti = ser._meta.Get( actualType );
// 1. Get/Create Instance
var (obj, _) = GetOrCreateInstance( ser, elem, actualType, existing );
if( obj == null )
return null;
// Handle graph refs (if already processed)
if( ser._cfg.Structure == Datastructure.Graph && elem.HasAttribute( "ref" ) )
{
long id = long.Parse( elem.GetAttribute( "ref" ) );
if( ser._processed.TryGetValue( id, out var processedObj ) )
return processedObj;
}
// Track if it's new
ser.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 = ser._resolver.ConvertSimple( valueSource.Value!, memberMeta.Type );
}
else // Child Element
{
memberValue = ser.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 ser, XmlWriter writer, object? obj, string name, Type memberType, bool forceType )
{
if( obj == null )
{ /* Write null */ return; }
writer.WriteStartElement( name );
ser.WriteTypeAttr( writer, memberType, obj.GetType() );
var ti = ser._meta.Get( obj.GetType() );
if( ser.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
{
ser.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 ser, XmlElement elem, Type type, object? existing )
{
long id = -1;
bool first = true;
// Check existing
if( existing != null && type.IsAssignableFrom( existing.GetType() ) )
{
id = ser._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 = ser._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