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:
parent
f873c42cbf
commit
3f850cc9b0
551
.editorconfig
Normal file
551
.editorconfig
Normal 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
|
||||
@ -54,8 +54,8 @@ namespace lib
|
||||
|
||||
static public void startup( string filename )
|
||||
{
|
||||
res.Mgr.register( load );
|
||||
res.Mgr.registerSub( typeof( ConfigBase ) );
|
||||
res.Mgr.Register( load );
|
||||
res.Mgr.RegisterSub( typeof( ConfigBase ) );
|
||||
|
||||
s_cfg = load<ConfigCfg>( filename );
|
||||
|
||||
|
||||
163
imm/FSM.cs
163
imm/FSM.cs
@ -1,103 +1,110 @@
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Optional;
|
||||
using imm; // Ensure this namespace is available
|
||||
|
||||
|
||||
|
||||
namespace imm;
|
||||
|
||||
|
||||
|
||||
public record class Context : imm.Recorded<Context>, Imm
|
||||
/// <summary>
|
||||
/// Base context for an FSM.
|
||||
/// 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>
|
||||
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
|
||||
where TSUB : State<TSUB, CTX>
|
||||
where CTX : Context
|
||||
/// <summary>
|
||||
/// Base state for an FSM.
|
||||
/// 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;
|
||||
|
||||
virtual public (CTX, TSUB) onEnter(CTX ctx, State<TSUB, CTX> oldState)
|
||||
/// <summary>
|
||||
/// Called when entering this state.
|
||||
/// </summary>
|
||||
public virtual (TCtx Context, TSelf State) OnEnter(TCtx context, FsmStateBase<TSelf, TCtx> oldState)
|
||||
{
|
||||
return (ctx, (TSUB)this);
|
||||
return (context, (TSelf)this);
|
||||
}
|
||||
|
||||
virtual public (CTX, TSUB) onExit(CTX ctx, State<TSUB, CTX> newState)
|
||||
/// <summary>
|
||||
/// Called when exiting this state.
|
||||
/// </summary>
|
||||
public virtual (TCtx Context, TSelf State) OnExit(TCtx context, FsmStateBase<TSelf, TCtx> newState)
|
||||
{
|
||||
return (ctx, (TSUB)this);
|
||||
}
|
||||
return (context, (TSelf)this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public record class FSM<TSUB, ST, CTX> : imm.Recorded<TSUB>, Imm
|
||||
where TSUB : FSM<TSUB, ST, CTX>
|
||||
where ST : State<ST, CTX>
|
||||
where CTX : Context
|
||||
{
|
||||
Meta Imm.Meta => base.Meta;
|
||||
|
||||
public CTX Context { get; init; }
|
||||
public ST State { get; init; }
|
||||
|
||||
public FSM( CTX context, ST stStart )
|
||||
{
|
||||
Context = context;
|
||||
State = stStart;
|
||||
// Required for 'with' expressions.
|
||||
protected FsmStateBase(Recorded<TSelf> original) : base(original) { }
|
||||
protected FsmStateBase() { }
|
||||
}
|
||||
|
||||
public TSUB Transition(ST newState, string reason,
|
||||
/// <summary>
|
||||
/// An immutable FSM base class.
|
||||
/// MUST inherit from Recorded<TSelf> or Timed<TSelf> in your concrete class.
|
||||
/// </summary>
|
||||
/// <typeparam name="TSelf">The concrete FSM type.</typeparam>
|
||||
/// <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>
|
||||
{
|
||||
public TCtx Context { get; init; }
|
||||
public TState State { get; init; }
|
||||
|
||||
protected FsmBase(TCtx initialContext, TState initialState)
|
||||
{
|
||||
Context = initialContext;
|
||||
State = initialState;
|
||||
}
|
||||
|
||||
// Required for 'with' expressions.
|
||||
protected FsmBase(Recorded<TSelf> original) : base(original)
|
||||
{
|
||||
var o = original as FsmBase<TSelf, TState, TCtx>;
|
||||
Context = o!.Context;
|
||||
State = o!.State;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transitions the FSM. It automatically uses the 'Process'
|
||||
/// method appropriate for Recorded or Timed, thanks to virtual overrides.
|
||||
/// </summary>
|
||||
public TSelf Transition(
|
||||
TState newState,
|
||||
string reason,
|
||||
[CallerMemberName] string memberName = "",
|
||||
[CallerFilePath] string filePath = "",
|
||||
[CallerLineNumber] int lineNumber = 0,
|
||||
[CallerArgumentExpression("newState")]
|
||||
string expression = default
|
||||
)
|
||||
[CallerArgumentExpression("newState")] string expression = "")
|
||||
{
|
||||
log.debug( $"Trans from {State.GetType().Name} to {newState.GetType().Name} for {reason}" );
|
||||
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
|
||||
// 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 = newCTX,
|
||||
State = storeState,
|
||||
}, $"{reason}" );
|
||||
|
||||
return newFSM;
|
||||
Context = ctxAfterEnter,
|
||||
State = stateAfterEnter
|
||||
},
|
||||
$"Transition to {newState.GetType().Name}: {reason}",
|
||||
memberName, filePath, lineNumber, expression
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
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;
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
|
||||
651
imm/Imm.cs
651
imm/Imm.cs
@ -1,404 +1,337 @@
|
||||
|
||||
|
||||
#nullable enable
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using lib;
|
||||
|
||||
namespace imm;
|
||||
|
||||
/*
|
||||
T O D O :
|
||||
T O D O :
|
||||
T O D O :
|
||||
x) Add unit tests for all this. This will definitely benefit from them
|
||||
*/
|
||||
/// <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; }
|
||||
|
||||
static public class Util
|
||||
{
|
||||
//This can handle both Timed and Recorded
|
||||
static public T Process<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 : Recorded<T>
|
||||
{
|
||||
obj = obj.Process( fn, reason, dbgName, dbgPath, dbgLine, dbgExp );
|
||||
return obj;
|
||||
/// <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 );
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
/// <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>
|
||||
{
|
||||
obj = obj.Process( fn, reason );
|
||||
/// <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 );
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
static public class ObjExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
|
||||
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 )]
|
||||
public record class Versioned<T> : Imm
|
||||
where T : Versioned<T>
|
||||
{
|
||||
|
||||
public delegate void ChangeDelegate( T? old, T next );
|
||||
|
||||
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;
|
||||
}
|
||||
*/
|
||||
|
||||
// --- Metadata Hierarchy ---
|
||||
|
||||
public T Process( Func<T, T> fn, string reason = "" )
|
||||
public interface VersionedMeta
|
||||
{
|
||||
var newT = fn( ( T )this );
|
||||
public uint Version { get; }
|
||||
public string Reason { get; }
|
||||
}
|
||||
|
||||
return newT with
|
||||
|
||||
/// <summary>
|
||||
/// Obj metadata for version tracking.
|
||||
/// </summary>
|
||||
public record Metadata_Versioned
|
||||
{
|
||||
MetaStorage = Meta with
|
||||
public uint Version { get; init; } = 1;
|
||||
public string Reason { get; init; } = "Created";
|
||||
}
|
||||
|
||||
|
||||
public interface RecordedMeta : VersionedMeta
|
||||
{
|
||||
Version = newT.Meta.Version + 1,
|
||||
public string MemberName { get; }
|
||||
public string FilePath { get; }
|
||||
public int LineNumber { get; }
|
||||
public string Expression { get; }
|
||||
}
|
||||
|
||||
|
||||
/// <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; } = "";
|
||||
}
|
||||
|
||||
public interface TimedMeta : RecordedMeta
|
||||
{
|
||||
public DateTime CreatedAt { get; }
|
||||
public DateTime TouchedAt { get; }
|
||||
}
|
||||
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
// --- Record Hierarchy ---
|
||||
|
||||
/// <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();
|
||||
|
||||
[DebuggerBrowsable( DebuggerBrowsableState.Never )]
|
||||
public ChangeDelegate<T> OnChange { get; set; } = ( o, n ) => { };
|
||||
|
||||
public virtual Obj? Old => null;
|
||||
|
||||
Metadata_Versioned Obj.Meta => this.Meta;
|
||||
Obj? Obj.Old => this.Old;
|
||||
|
||||
public Versioned() { }
|
||||
protected Versioned( Versioned<T> original )
|
||||
{
|
||||
OnChange = original.OnChange;
|
||||
Meta = original.Meta;
|
||||
}
|
||||
|
||||
public virtual T Process(
|
||||
Func<T, T> fn,
|
||||
string reason = "Processed",
|
||||
[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 newVersion = next with
|
||||
{
|
||||
Meta = new Metadata_Versioned { /*...*/ },
|
||||
OnChange = current.OnChange
|
||||
};
|
||||
newVersion.OnChange( current, newVersion );
|
||||
return newVersion;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 );
|
||||
|
||||
/// <summary>
|
||||
/// Implements Obj.Record by calling the virtual T Record.
|
||||
/// </summary>
|
||||
Obj Obj.Record(
|
||||
string reason = "Recorded",
|
||||
[CallerMemberName] string memberName = "",
|
||||
[CallerFilePath] string filePath = "",
|
||||
[CallerLineNumber] int lineNumber = 0 ) => this.Record( reason, memberName, filePath, lineNumber );
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// <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,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
where T : Recorded<T>
|
||||
{
|
||||
|
||||
new public record class MetaData : Versioned<T>.MetaData
|
||||
{
|
||||
[lib.Dont]
|
||||
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
|
||||
}
|
||||
MemberName = memberName,
|
||||
FilePath = filePath,
|
||||
LineNumber = lineNumber,
|
||||
Expression = expStr,
|
||||
OldObject = current
|
||||
};
|
||||
|
||||
OnChange( orig, ret );
|
||||
|
||||
return ret;
|
||||
}
|
||||
var newVersion = next with { Meta = newMeta, OnChange = current.OnChange };
|
||||
newVersion.OnChange(current, newVersion);
|
||||
return newVersion;
|
||||
}
|
||||
|
||||
public record class Timed<T> : Recorded<T>, imm.Imm
|
||||
where T : Timed<T>
|
||||
public new T Record(
|
||||
string reason = "Recorded",
|
||||
[CallerMemberName] string memberName = "",
|
||||
[CallerFilePath] string filePath = "",
|
||||
[CallerLineNumber] int lineNumber = 0)
|
||||
{
|
||||
|
||||
new public record class MetaData : Recorded<T>.MetaData
|
||||
{
|
||||
public readonly DateTime CreatedAt = DateTime.Now;
|
||||
public DateTime TouchedAt { get; internal set; } = DateTime.Now;
|
||||
return Process(t => t, reason, memberName, filePath, lineNumber );
|
||||
}
|
||||
|
||||
public Timed() : this( new MetaData() )
|
||||
{
|
||||
}
|
||||
|
||||
public Timed( MetaData meta ) : base( meta )
|
||||
/// <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;
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
new public MetaData Meta => MetaStorage as MetaData ?? new MetaData();
|
||||
public Timed() { }
|
||||
protected Timed(Timed<T> original) : base(original) { Meta = original.Meta; }
|
||||
|
||||
public TimeSpan Since => Meta.TouchedAt - Meta.Old?.Meta.TouchedAt ?? TimeSpan.MaxValue;
|
||||
|
||||
public void CallOnChange()
|
||||
{
|
||||
OnChange( Meta.Old, (T)this );
|
||||
}
|
||||
|
||||
override public T Record(
|
||||
public override T Process(
|
||||
Func<T, T> fn,
|
||||
string reason = "",
|
||||
[CallerMemberName] string dbgName = "",
|
||||
[CallerFilePath] string dbgPath = "",
|
||||
[CallerLineNumber] int dbgLine = 0
|
||||
)
|
||||
[CallerMemberName] string memberName = "",
|
||||
[CallerFilePath] string filePath = "",
|
||||
[CallerLineNumber] int lineNumber = 0,
|
||||
[CallerArgumentExpression("fn")] string expression = "")
|
||||
{
|
||||
return Process( t => t with { MetaStorage = t.Meta with { Reason = $"Record {reason}" }}, reason, dbgName, dbgPath, dbgLine );
|
||||
}
|
||||
var current = (T)this;
|
||||
var next = fn(current);
|
||||
|
||||
override public T Process( T next,
|
||||
string reason = "",
|
||||
[CallerMemberName] string dbgName = "",
|
||||
[CallerFilePath] string dbgPath = "",
|
||||
[CallerLineNumber] int dbgLine = 0,
|
||||
[CallerArgumentExpression("next")]
|
||||
string dbgExp = ""
|
||||
)
|
||||
if (ReferenceEquals(current, next)) return current;
|
||||
|
||||
var newMeta = new Metadata_Timed
|
||||
{
|
||||
return ProcessWork( ( old ) => next, reason, dbgName, dbgPath, dbgLine, dbgExp );
|
||||
}
|
||||
|
||||
public U ProcessFn<U>( Func<U, U> fn,
|
||||
string reason = "",
|
||||
[CallerMemberName] string dbgName = "",
|
||||
[CallerFilePath] string dbgPath = "",
|
||||
[CallerLineNumber] int dbgLine = 0,
|
||||
[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,
|
||||
string reason = "",
|
||||
[CallerMemberName] string dbgName = "",
|
||||
[CallerFilePath] string dbgPath = "",
|
||||
[CallerLineNumber] int dbgLine = 0,
|
||||
[CallerArgumentExpression("fn")]
|
||||
string dbgExp = ""
|
||||
)
|
||||
=> ProcessWork( fn, reason, dbgName, dbgPath, dbgLine, dbgExp );
|
||||
|
||||
override 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
|
||||
{
|
||||
MetaStorage = Meta with
|
||||
{
|
||||
//Versioned
|
||||
Version = orig.Meta.Version + 1,
|
||||
Reason = !string.IsNullOrWhiteSpace( reason ) ? reason : next.Meta.Reason,
|
||||
|
||||
//Recorded
|
||||
DbgName = dbgName,
|
||||
DbgPath = dbgPath,
|
||||
DbgLine = dbgLine,
|
||||
DbgExp = dbgExp,
|
||||
Old_backing = orig,
|
||||
|
||||
//Timed
|
||||
TouchedAt = DateTime.Now,
|
||||
}
|
||||
|
||||
Version = current.Meta.Version + 1,
|
||||
Reason = reason,
|
||||
MemberName = memberName,
|
||||
FilePath = filePath,
|
||||
LineNumber = lineNumber,
|
||||
Expression = expression,
|
||||
OldObject = current,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
TouchedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
if( OnChange != null)
|
||||
OnChange( orig, ret );
|
||||
|
||||
return ret;
|
||||
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 );
|
||||
}
|
||||
}
|
||||
|
||||
199
imm/List.cs
199
imm/List.cs
@ -1,158 +1,77 @@
|
||||
using System;
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace imm;
|
||||
|
||||
/// <summary>
|
||||
/// An immutable list implementation that tracks history, metadata, and time.
|
||||
/// </summary>
|
||||
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 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>.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>.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 );
|
||||
}
|
||||
#endregion
|
||||
|
||||
// --- Standard Interfaces ---
|
||||
public IEnumerator<T> GetEnumerator() => Values.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)Values).GetEnumerator();
|
||||
}
|
||||
83
imm/iu.cs
83
imm/iu.cs
@ -1,56 +1,47 @@
|
||||
|
||||
// A spot for immutable helpers
|
||||
|
||||
// TODO
|
||||
// TODO
|
||||
// TODO
|
||||
// x) Wrap metadata into its own struct
|
||||
// x) Make metadata a struct vs a class
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using imm;
|
||||
|
||||
|
||||
static public class iu
|
||||
/// <summary>
|
||||
/// 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
|
||||
static public T Process<T>( ref T obj, Func<T, T> fn,
|
||||
string reason = "",
|
||||
[CallerMemberName] string dbgName = "",
|
||||
[CallerFilePath] string dbgPath = "",
|
||||
[CallerLineNumber] int dbgLine = 0,
|
||||
[CallerArgumentExpression("fn")]
|
||||
string dbgExpression = default )
|
||||
where T : imm.Recorded<T>, imm.Imm
|
||||
{
|
||||
obj = obj.Process( fn, reason, dbgName, dbgPath, dbgLine, dbgExpression );
|
||||
return obj;
|
||||
}
|
||||
|
||||
static public imm.Timed<T> Process<T>( ref imm.Timed<T> obj, Func<T, T> fn,
|
||||
string reason = "",
|
||||
[CallerMemberName] string dbgName = "",
|
||||
[CallerFilePath] string dbgPath = "",
|
||||
[CallerLineNumber] int dbgLine = 0,
|
||||
[CallerArgumentExpression("fn")]
|
||||
string dbgExpression = default )
|
||||
where T : imm.Timed<T>, imm.Imm
|
||||
{
|
||||
obj = obj.Process( fn, reason, dbgName, dbgPath, dbgLine, dbgExpression );
|
||||
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 dbgExpression = default )
|
||||
where T : imm.Versioned<T>, imm.Imm
|
||||
/// <summary>
|
||||
/// Processes a 'Versioned' object (Level 1).
|
||||
/// </summary>
|
||||
public static T LightProcess<T>(
|
||||
ref T obj,
|
||||
Func<T, T> fn,
|
||||
string reason = "Processed")
|
||||
where T : Versioned<T>
|
||||
{
|
||||
obj = obj.Process(fn, reason);
|
||||
return obj;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes a 'Recorded' object (Level 2), capturing caller info.
|
||||
/// </summary>
|
||||
public static T Process<T>(
|
||||
ref T obj,
|
||||
Func<T, T> fn,
|
||||
string reason = "",
|
||||
[CallerMemberName] string dbgName = "",
|
||||
[CallerFilePath] string dbgPath = "",
|
||||
[CallerLineNumber] int dbgLine = 0,
|
||||
[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>
|
||||
// and its Process override handles the timing.
|
||||
}
|
||||
122
logging/Log.cs
122
logging/Log.cs
@ -129,7 +129,7 @@ static public class log
|
||||
|
||||
static void StartGCWatcher()
|
||||
{
|
||||
while( !s_running )
|
||||
while( s_threading != ThreadState.Running )
|
||||
{
|
||||
Thread.Sleep( 10 );
|
||||
}
|
||||
@ -140,7 +140,7 @@ static public class log
|
||||
|
||||
static void StartTracing()
|
||||
{
|
||||
while( !s_running )
|
||||
while( s_threading != ThreadState.Running )
|
||||
{
|
||||
Thread.Sleep( 10 );
|
||||
}
|
||||
@ -516,12 +516,12 @@ static public class log
|
||||
lock( s_lock )
|
||||
{
|
||||
//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" );
|
||||
}
|
||||
|
||||
s_running = true;
|
||||
s_threading = ThreadState.Running;
|
||||
|
||||
|
||||
s_startTime = DateTime.Now;
|
||||
@ -569,12 +569,7 @@ static public class log
|
||||
LogEvent msgFilename = new LogEvent( LogType.Info, $"Logging in {filename}", "", 0, "", "log.startup", "", null );
|
||||
writeToAll( msgFilename );
|
||||
|
||||
var start = new ThreadStart( run );
|
||||
|
||||
s_thread = new Thread( start );
|
||||
s_thread.Priority = ThreadPriority.BelowNormal;
|
||||
s_thread.Name = $"Logging";
|
||||
s_thread.Start();
|
||||
StartThread();
|
||||
|
||||
|
||||
|
||||
@ -605,17 +600,49 @@ static public class log
|
||||
|
||||
}
|
||||
|
||||
static bool s_running = false;
|
||||
|
||||
static void run()
|
||||
private static void StartThread()
|
||||
{
|
||||
while( !s_running )
|
||||
var start = new ThreadStart( threadLoop );
|
||||
|
||||
s_thread = new Thread( start );
|
||||
s_thread.Priority = ThreadPriority.BelowNormal;
|
||||
s_thread.Name = $"Logging";
|
||||
s_thread.Start();
|
||||
}
|
||||
|
||||
private static void StopThread( ThreadState thread )
|
||||
{
|
||||
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
|
||||
Thread.Sleep( 1 );
|
||||
}
|
||||
|
||||
while( s_running )
|
||||
while( s_threading != ThreadState.Running )
|
||||
{
|
||||
while( s_events.TryDequeue( out var evt ) )
|
||||
{
|
||||
@ -626,22 +653,18 @@ static public class log
|
||||
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 );
|
||||
var endLine = new LogEvent( LogType.Raw, $"Thread state {s_threading} ******************************************************************************************************************", "", 0, "", "lib.time", "", null );
|
||||
writeToAll( endLine );
|
||||
|
||||
}
|
||||
|
||||
|
||||
public static void stop()
|
||||
{
|
||||
if( !s_running ) return;
|
||||
if( s_threading == ThreadState.Finished )
|
||||
return;
|
||||
|
||||
s_running = false;
|
||||
|
||||
while( s_thread.IsAlive )
|
||||
{
|
||||
Thread.Sleep( 0 );
|
||||
}
|
||||
StopThread( ThreadState.Finished );
|
||||
|
||||
s_writer?.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 )
|
||||
{
|
||||
s_delegates.Add( cb );
|
||||
@ -660,14 +701,22 @@ static public class log
|
||||
{
|
||||
switch( type )
|
||||
{
|
||||
case LogType.Trace: return ' ';
|
||||
case LogType.Debug: return ' ';
|
||||
case LogType.Info: return ' ';
|
||||
case LogType.High: return '+';
|
||||
case LogType.Warn: return '+';
|
||||
case LogType.Error: return '*';
|
||||
case LogType.Fatal: return '*';
|
||||
default: return '?';
|
||||
case LogType.Trace:
|
||||
return ' ';
|
||||
case LogType.Debug:
|
||||
return ' ';
|
||||
case LogType.Info:
|
||||
return ' ';
|
||||
case LogType.High:
|
||||
return '+';
|
||||
case LogType.Warn:
|
||||
return '+';
|
||||
case LogType.Error:
|
||||
return '*';
|
||||
case LogType.Fatal:
|
||||
return '*';
|
||||
default:
|
||||
return '?';
|
||||
}
|
||||
}
|
||||
|
||||
@ -750,17 +799,20 @@ static public class log
|
||||
|
||||
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.
|
||||
//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" )}";
|
||||
|
||||
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 );
|
||||
|
||||
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;
|
||||
|
||||
636
res/Resource.cs
636
res/Resource.cs
@ -1,444 +1,434 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
//using System.Threading.Tasks;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Collections.Immutable;
|
||||
using System.Threading;
|
||||
using System.IO;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using Microsoft.CodeAnalysis; // Note: This was in the original, but seems unused. Keep for now.
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace res;
|
||||
|
||||
// A delegate representing a function that can load a resource of type T.
|
||||
public delegate T Load<out T>( string filename );
|
||||
|
||||
using ImmDefLoad = ImmutableQueue<(string name, Ref)>;
|
||||
|
||||
public interface Res_old
|
||||
/// <summary>
|
||||
/// Abstract base class for a resource reference.
|
||||
/// Provides a common way to refer to resources and includes debugging/tracking info.
|
||||
/// </summary>
|
||||
[DebuggerDisplay( "Path = {Filename}" )]
|
||||
public abstract class Ref
|
||||
{
|
||||
public static bool VerboseLogging { get; set; } = false;
|
||||
|
||||
}
|
||||
public string Filename { get; }
|
||||
public string Reason { get; }
|
||||
public string DbgName { get; }
|
||||
public string DbgPath { get; }
|
||||
public int DbgLine { get; }
|
||||
|
||||
[DebuggerDisplay("Path = {path}")]
|
||||
abstract public class Ref : lib.I_Serialize
|
||||
{
|
||||
static public bool s_verboseLogging = false;
|
||||
|
||||
public string Filename =>path;
|
||||
|
||||
|
||||
public Ref( string filename = "{empty_filename}",
|
||||
protected Ref(
|
||||
string filename,
|
||||
string reason = "",
|
||||
[CallerMemberName] string dbgName = "",
|
||||
[CallerFilePath] string dbgPath = "",
|
||||
[CallerLineNumber] int dbgLine = 0
|
||||
)
|
||||
[CallerLineNumber] int dbgLine = 0 )
|
||||
{
|
||||
path = filename;
|
||||
if( s_verboseLogging ) log.info( $"Ref: {GetType().Name} {path}" );
|
||||
Filename = filename;
|
||||
Reason = reason;
|
||||
DbgName = dbgName;
|
||||
DbgPath = dbgPath;
|
||||
DbgLine = dbgLine;
|
||||
|
||||
_reason = reason;
|
||||
_dbgName = dbgName;
|
||||
_dbgPath = dbgPath;
|
||||
_dbgLine = dbgLine;
|
||||
if( VerboseLogging )
|
||||
log.info( $"Ref Created: {GetType().Name} {Filename}" );
|
||||
}
|
||||
|
||||
abstract public object lookup(
|
||||
/// <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
|
||||
|
||||
);
|
||||
|
||||
|
||||
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 = "";
|
||||
[CallerLineNumber] int dbgLine = 0 );
|
||||
|
||||
/// <summary>
|
||||
/// Called when the resource might have changed (optional, base implementation does nothing).
|
||||
/// </summary>
|
||||
public virtual void OnChange() { }
|
||||
|
||||
/// <summary>
|
||||
/// Internal method to trigger the initial load (used by deferred loading, if implemented).
|
||||
/// </summary>
|
||||
internal virtual void InternalLoad() { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A typed reference to a resource of type T.
|
||||
/// Handles lazy loading and access to the resource.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
[DebuggerDisplay("Path = {path} / Res = {res}")]
|
||||
[DebuggerDisplay( "Path = {Filename} / Res = {m_res}" )]
|
||||
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>
|
||||
/// Gets the resource, loading it if necessary.
|
||||
/// </summary>
|
||||
public T Res => m_res ?? Lookup();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the resource, loading it if necessary.
|
||||
/// </summary>
|
||||
//[Deprecated("Use Res property instead.")]
|
||||
public T res => m_res ?? Lookup();
|
||||
|
||||
public Ref(
|
||||
string filename = "",
|
||||
string reason = "",
|
||||
[CallerMemberName] string dbgName = "",
|
||||
[CallerFilePath] string dbgPath = "",
|
||||
[CallerLineNumber] int dbgLine = 0
|
||||
|
||||
)
|
||||
[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( s_verboseLogging ) log.info( $"Ref.lookup {GetType().Name} {GetType().GenericTypeArguments[0]} path {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}" );
|
||||
if( VerboseLogging )
|
||||
log.info( $"Ref<T> Created: {GetType().Name}<{typeof( T ).Name}> {Filename}" );
|
||||
m_res = default;
|
||||
}
|
||||
|
||||
|
||||
override internal void load()
|
||||
{
|
||||
m_res = Mgr.load<T>( Filename, _reason, _dbgName, _dbgPath, _dbgLine );
|
||||
if( s_verboseLogging ) log.info( $"Ref.load {GetType().Name} {GetType().GenericTypeArguments[0]} path {Filename}" );
|
||||
}
|
||||
|
||||
public object OnDeserialize( object enclosing )
|
||||
{
|
||||
return enclosing;
|
||||
}
|
||||
|
||||
static public Ref<T> createAsset( T v, string path,
|
||||
/// <summary>
|
||||
/// Looks up and loads the resource.
|
||||
/// </summary>
|
||||
public override T Lookup(
|
||||
string reason = "",
|
||||
[CallerMemberName] string dbgName = "",
|
||||
[CallerFilePath] string dbgPath = "",
|
||||
[CallerLineNumber] int dbgLine = 0
|
||||
)
|
||||
[CallerLineNumber] int dbgLine = 0 )
|
||||
{
|
||||
// If already loaded, return it.
|
||||
if( m_res != null )
|
||||
return m_res;
|
||||
|
||||
// Load using the Mgr.
|
||||
m_res = Mgr.Load<T>( Filename, $"Ref lookup (orig: {Reason}) bcs {reason}", dbgName, dbgPath, dbgLine );
|
||||
if( VerboseLogging )
|
||||
log.info( $"Ref.Lookup: {GetType().Name}<{typeof( T ).Name}> {Filename}" );
|
||||
return m_res;
|
||||
}
|
||||
|
||||
/*
|
||||
/// <summary>
|
||||
/// Overrides the base Lookup to provide a typed result.
|
||||
/// </summary>
|
||||
public override object Lookup(
|
||||
string reason = "",
|
||||
[CallerMemberName] string dbgName = "",
|
||||
[CallerFilePath] string dbgPath = "",
|
||||
[CallerLineNumber] int dbgLine = 0)
|
||||
{
|
||||
return Lookup(reason, dbgName, dbgPath, dbgLine);
|
||||
}
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Internal load implementation.
|
||||
/// </summary>
|
||||
internal override void InternalLoad()
|
||||
{
|
||||
if( m_res == null )
|
||||
{
|
||||
m_res = Mgr.Load<T>( Filename, Reason, DbgName, DbgPath, DbgLine );
|
||||
if( VerboseLogging )
|
||||
log.info( $"Ref.InternalLoad: {GetType().Name}<{typeof( T ).Name}> {Filename}" );
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new resource asset and saves it, returning a Ref to it.
|
||||
/// Handles existing files by renaming them.
|
||||
/// </summary>
|
||||
public static Ref<T> CreateAsset(
|
||||
T value, string path,
|
||||
string reason = "",
|
||||
[CallerMemberName] string dbgName = "",
|
||||
[CallerFilePath] string dbgPath = "",
|
||||
[CallerLineNumber] int dbgLine = 0 )
|
||||
{
|
||||
if( File.Exists( path ) )
|
||||
{
|
||||
log.warn( $"For {typeof(T).Name}, saving asset to {path}, but it already exists" );
|
||||
|
||||
var newPath = $"{path}_{DateTime.Now.ToShortDateString()}_{DateTime.Now.ToShortTimeString()}";
|
||||
|
||||
System.IO.File.Move(path, newPath );
|
||||
|
||||
log.warn( $"For {typeof(T).Name}, renamed to {newPath}" );
|
||||
log.warn( $"Asset exists: {path}. Renaming before creating." );
|
||||
var newPath = $"{path}_{DateTime.Now:yyyyMMdd_HHmmss}";
|
||||
File.Move( path, newPath );
|
||||
log.warn( $"Renamed existing asset to: {newPath}" );
|
||||
}
|
||||
|
||||
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 )
|
||||
{
|
||||
createReason = $"{imm?.Meta}";
|
||||
}
|
||||
// We need a way to save 'value' to 'path' before creating the Ref.
|
||||
// This part requires a 'Save' function, which isn't in the original.
|
||||
// Let's assume you'll add saving logic here.
|
||||
// Mgr.Save(value, path); // Example: Needs implementation
|
||||
|
||||
var 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;
|
||||
}
|
||||
|
||||
[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();
|
||||
}
|
||||
|
||||
public delegate T Load<out T>( string filename );
|
||||
|
||||
|
||||
abstract class LoadHolder
|
||||
// Internal holder for type-specific loaders.
|
||||
private abstract class LoadHolder { public abstract object Load( string filename ); }
|
||||
private class LoadHolder<T> : LoadHolder
|
||||
{
|
||||
public abstract object load();
|
||||
private readonly Load<T> _fnLoad;
|
||||
public LoadHolder( Load<T> fnLoad ) { _fnLoad = fnLoad; }
|
||||
public override object Load( string filename ) => _fnLoad( filename )!;
|
||||
}
|
||||
|
||||
|
||||
class LoadHolder<T> : LoadHolder
|
||||
{
|
||||
public LoadHolder( Load<T> fnLoad )
|
||||
{
|
||||
_fnLoad = fnLoad;
|
||||
}
|
||||
|
||||
public Load<T> _fnLoad;
|
||||
|
||||
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()
|
||||
// Cache for resource holders (contains WeakReferences).
|
||||
private static class ResCache<T> where T : class, new()
|
||||
{
|
||||
public static T s_default = new();
|
||||
public static ImmutableDictionary<string, ResourceHolder<T>> s_cache = ImmutableDictionary<string, ResourceHolder<T>>.Empty;
|
||||
}
|
||||
|
||||
private static ImmutableDictionary<Type, LoadHolder> s_loaders = ImmutableDictionary<Type, LoadHolder>.Empty;
|
||||
// Using ConcurrentDictionary for per-file locking objects.
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, object> s_loadingLocks = new();
|
||||
|
||||
public class Mgr
|
||||
/// <summary>
|
||||
/// Registers a specific loading function for a type T.
|
||||
/// </summary>
|
||||
public static void Register<T>( Load<T> loader ) where T : class, new()
|
||||
{
|
||||
|
||||
static public void startup()
|
||||
var type = typeof( T );
|
||||
if( s_loaders.ContainsKey( type ) )
|
||||
{
|
||||
log.warn( $"Loader for type {type.Name} is already registered. Overwriting." );
|
||||
}
|
||||
ImmutableInterlocked.Update( ref s_loaders, d => d.SetItem( type, new LoadHolder<T>( loader ) ) );
|
||||
log.info( $"Registered loader for {type.Name}" );
|
||||
}
|
||||
|
||||
static public void register<T>( Load<T> loader )
|
||||
/// <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 )
|
||||
{
|
||||
Debug.Assert( !Resource.mgr.m_loaders.ContainsKey( typeof( T ) ) );
|
||||
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 );
|
||||
|
||||
var lh = new LoadHolder<T>( loader );
|
||||
|
||||
ImmutableInterlocked.TryAdd( ref Resource.mgr.m_loaders, typeof( T ), lh );
|
||||
if( genericLoadMethod == null )
|
||||
{
|
||||
log.error( $"Could not find 'public static T res_load<T>(string filename)' method on {baseType.Name} or its bases." );
|
||||
return;
|
||||
}
|
||||
|
||||
//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 )
|
||||
{
|
||||
foreach( var ass in AppDomain.CurrentDomain.GetAssemblies() )
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach( var t in ass.GetTypes() )
|
||||
{
|
||||
if( !baseType.IsAssignableFrom( t ) )
|
||||
continue;
|
||||
if( t.IsClass && !t.IsAbstract && baseType.IsAssignableFrom( t ) && t != baseType )
|
||||
{
|
||||
try
|
||||
{
|
||||
log.debug( $"Found subtype {t.Name}, creating loader." );
|
||||
var concreteLoadMethod = genericLoadMethod.MakeGenericMethod( t );
|
||||
var loadDelegateType = typeof( Load<> ).MakeGenericType( t );
|
||||
var loaderDelegate = Delegate.CreateDelegate( loadDelegateType, concreteLoadMethod );
|
||||
|
||||
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;
|
||||
var mi_ng = mi.MakeGenericMethod( typeParams );
|
||||
|
||||
var loadGenType = typeof( Load<> );
|
||||
|
||||
var loadType = loadGenType.MakeGenericType( t );
|
||||
|
||||
var loader = Delegate.CreateDelegate( loadType, mi_ng );
|
||||
|
||||
var lhGenType = typeof( LoadHolder<> );
|
||||
|
||||
var lhType = lhGenType.MakeGenericType( t );
|
||||
|
||||
var lh = Activator.CreateInstance( lhType, loader ) as LoadHolder;
|
||||
|
||||
ImmutableInterlocked.TryAdd( ref Resource.mgr.m_loaders!, t, lh );
|
||||
if( loadHolder != null )
|
||||
{
|
||||
if( !ImmutableInterlocked.TryAdd( ref s_loaders, t, loadHolder ) )
|
||||
{
|
||||
log.debug( $"Loader for {t.Name} already exists, skipping." );
|
||||
}
|
||||
else
|
||||
{
|
||||
log.info( $"Registered loader for subtype {t.Name}." );
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
catch( Exception ex )
|
||||
{
|
||||
log.error( $"Failed to create loader for subtype {t.Name}: {ex.Message}" );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( ReflectionTypeLoadException rtle )
|
||||
{
|
||||
log.warn( $"Could not fully scan assembly {ass.FullName}: {rtle.Message}" );
|
||||
}
|
||||
catch( Exception ex )
|
||||
{
|
||||
log.warn( $"Error scanning assembly {ass.FullName}: {ex.Message}" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static public Ref<T> lookup<T>( string filename,
|
||||
/// <summary>
|
||||
/// Creates a Ref<T> for a given filename.
|
||||
/// </summary>
|
||||
public static Ref<T> Lookup<T>(
|
||||
string filename,
|
||||
string reason = "",
|
||||
[CallerMemberName] string dbgName = "",
|
||||
[CallerFilePath] string dbgPath = "",
|
||||
[CallerLineNumber] int dbgLine = 0
|
||||
) where T : class, new()
|
||||
[CallerLineNumber] int dbgLine = 0 ) where T : class, new()
|
||||
{
|
||||
return new Ref<T>( filename, reason, dbgName, dbgPath, dbgLine );
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
static public Ref lookup( string filename, Type t,
|
||||
/// <summary>
|
||||
/// Loads a resource, handling caching and thread-safe loading.
|
||||
/// </summary>
|
||||
public static T Load<T>(
|
||||
string filename,
|
||||
string reason = "",
|
||||
[CallerMemberName] string dbgName = "",
|
||||
[CallerFilePath] string dbgPath = "",
|
||||
[CallerLineNumber] int dbgLine = 0
|
||||
)
|
||||
[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;
|
||||
}
|
||||
|
||||
// @@@ TODO Pass information through here
|
||||
static public T? load<T>( string filename ) where T : class
|
||||
// 2. Get a lock specific to this filename and lock it.
|
||||
var fileLock = s_loadingLocks.GetOrAdd( filename, _ => new object() );
|
||||
lock( fileLock )
|
||||
{
|
||||
// 3. Double-check cache *inside* the lock
|
||||
if( TryGetFromCache<T>( filename, out cachedValue ) )
|
||||
{
|
||||
return cachedValue;
|
||||
}
|
||||
|
||||
// 4. Perform the actual load
|
||||
log.warn( $"Loading {typeof( T ).Name}: {filename} ({reason} at {dbgName}:{dbgLine})" );
|
||||
var newValue = ActualLoad<T>( filename, reason, dbgName, dbgPath, dbgLine );
|
||||
|
||||
// 5. Cache the new value
|
||||
CacheResource( filename, newValue, reason );
|
||||
|
||||
return newValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manually adds or updates a resource in the cache.
|
||||
/// </summary>
|
||||
internal static void CacheResource<T>( string filename, T resource, string reason ) where T : class, new()
|
||||
{
|
||||
var weak = new WeakReference<T>( resource );
|
||||
var holder = new ResourceHolder<T>( weak, filename, DateTime.Now );
|
||||
ImmutableInterlocked.Update( ref ResCache<T>.s_cache, d => d.SetItem( filename, holder ) );
|
||||
log.info( $"Cached {typeof( T ).Name}: {filename} ({reason})" );
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Tries to retrieve a resource from the cache.
|
||||
/// </summary>
|
||||
private static bool TryGetFromCache<T>( string filename, out T value ) where T : class, new()
|
||||
{
|
||||
if( ResCache<T>.s_cache.TryGetValue( filename, out var holder ) )
|
||||
{
|
||||
|
||||
if( holder.weak.TryGetTarget( out var v ) )
|
||||
if( holder.WeakRef.TryGetTarget( out var v ) )
|
||||
{
|
||||
return v;
|
||||
value = v;
|
||||
return true;
|
||||
}
|
||||
log.info( $"{filename} was in cache but dropped (GC'd), needs reloading." );
|
||||
}
|
||||
value = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
log.info( $"{filename} was in cache, but its been dropped, reloading." );
|
||||
}
|
||||
|
||||
log.warn( $"Block Loading {filename}." );
|
||||
|
||||
var newV = actualLoad<T>( filename );
|
||||
|
||||
return newV;
|
||||
}
|
||||
*/
|
||||
|
||||
static public T load<T>( string filename,
|
||||
/// <summary>
|
||||
/// The core loading logic. Must be called within a lock.
|
||||
/// </summary>
|
||||
private static T ActualLoad<T>(
|
||||
string filename,
|
||||
string reason = "",
|
||||
[CallerMemberName] string dbgName = "",
|
||||
[CallerFilePath] string dbgPath = "",
|
||||
[CallerLineNumber] int dbgLine = 0
|
||||
) where T : class, new()
|
||||
{
|
||||
if( ResCache<T>.s_cache.TryGetValue( filename, out var holder ) )
|
||||
if( s_loaders.TryGetValue( typeof( T ), out var loaderHolder ) )
|
||||
{
|
||||
if( holder.weak.TryGetTarget( out var v ) )
|
||||
var loadedObject = loaderHolder.Load( filename );
|
||||
if( loadedObject is T value )
|
||||
{
|
||||
return v;
|
||||
var meta = (value as imm.Obj)?.Meta;
|
||||
|
||||
// If it's an immutable object, record its loading.
|
||||
if( value is imm.Obj imm )
|
||||
{
|
||||
return (T)imm.Record( $"Loading bcs {reason}", dbgName, dbgPath, dbgLine );
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
lock(s_loadingLock)
|
||||
{
|
||||
if( s_loading.TryGetValue( filename, out var evt ) )
|
||||
{
|
||||
if( ResCache<T>.s_cache.TryGetValue( filename, out var holder ) )
|
||||
{
|
||||
if( holder.weak.TryGetTarget( out var v ) )
|
||||
{
|
||||
log.trace( $"{typeof(T).Name} loading {filename}" );
|
||||
return v;
|
||||
}
|
||||
|
||||
log.error( $"{filename} was in cache, but its been dropped, reloading." );
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
if( Resource.mgr.m_loaders.TryGetValue( typeof( T ), out var loaderGen ) )
|
||||
{
|
||||
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;
|
||||
return value;
|
||||
}
|
||||
else
|
||||
{
|
||||
log.error( $"Loader could not be found for type {typeof( T )}" );
|
||||
|
||||
log.error( $"Loader for {typeof( T ).Name} returned wrong type: {loadedObject?.GetType().Name}" );
|
||||
return ResCache<T>.s_default;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
log.error( $"Loader could not be found for type {typeof( T ).Name} for file {filename}" );
|
||||
return ResCache<T>.s_default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//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 );
|
||||
|
||||
if( ImmutableInterlocked.TryDequeue( ref s_deferredLoad, out var v ) )
|
||||
{
|
||||
v.Item2.load();
|
||||
}
|
||||
}
|
||||
}
|
||||
//*/
|
||||
|
||||
|
||||
|
||||
ImmutableDictionary<Type, LoadHolder> m_loaders = ImmutableDictionary<Type, LoadHolder>.Empty;
|
||||
|
||||
//Thread m_deferredLoader;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ using System.Reflection;
|
||||
namespace lib
|
||||
{
|
||||
//Old, use 2 now.
|
||||
class XmlFormatter: IFormatter
|
||||
class XmlFormatter_BAD: IFormatter
|
||||
{
|
||||
StreamingContext m_context;
|
||||
//SerializationMode m_mode;
|
||||
@ -45,6 +45,7 @@ namespace lib
|
||||
object OnDeserialize( object enclosing ) => this;
|
||||
}
|
||||
|
||||
|
||||
[Flags]
|
||||
public enum Types
|
||||
{
|
||||
@ -543,7 +544,7 @@ namespace lib
|
||||
{
|
||||
if( _cfg.VerboseLogging ) log.info( $"" );
|
||||
|
||||
var isImm = typeof(imm.Imm).IsAssignableFrom( narrowType );
|
||||
var isImm = typeof(imm.Obj).IsAssignableFrom( narrowType );
|
||||
|
||||
XmlNodeList allChildren = elem.ChildNodes;
|
||||
|
||||
@ -701,7 +702,7 @@ namespace lib
|
||||
}
|
||||
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}" );
|
||||
return newObj;
|
||||
}
|
||||
@ -1343,7 +1344,7 @@ namespace lib
|
||||
HashSet<string> whitelistFields, 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 )
|
||||
{
|
||||
|
||||
917
ser/XmlSer.cs
Normal file
917
ser/XmlSer.cs
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user