diff --git a/.editorconfig b/.editorconfig index b5e519b8..1f017bdd 100644 --- a/.editorconfig +++ b/.editorconfig @@ -20,29 +20,3 @@ dotnet_style_qualification_for_field=false:warning dotnet_style_qualification_for_method=false:warning dotnet_style_qualification_for_property=false:warning dotnet_style_require_accessibility_modifiers=for_non_interface_members:hint - -# ReSharper properties -resharper_csharp_max_line_length=150 -resharper_force_control_statements_braces=only_for_multiline -resharper_indent_switch_labels=true -resharper_js_brace_style=next_line -resharper_js_empty_block_style=together_same_line -resharper_js_keep_blank_lines_in_code=1 -resharper_js_stick_comment=false -resharper_js_wrap_chained_method_calls=chop_if_long -resharper_keep_blank_lines_between_declarations=1 -resharper_min_blank_lines_after_imports=1 -resharper_new_line_before_catch=true -resharper_new_line_before_else=true -resharper_new_line_before_finally=true -resharper_new_line_before_while=true -resharper_place_accessorholder_attribute_on_same_line=False -resharper_simple_embedded_statement_style=line_break -resharper_single_statement_function_style=line_break -resharper_space_before_method_parentheses_anonymous=true -resharper_space_within_empty_braces=false -resharper_space_within_single_line_array_initializer_braces=true -resharper_wrap_chained_binary_expressions=wrap_if_long -resharper_xml_alignment_tab_fill_style=use_tabs_only -resharper_xml_indent_style=tab -resharper_xml_use_indent_from_vs=false diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 146d7a44..94fab1a3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,10 +1,10 @@ version: 2 updates: -- package-ecosystem: nuget - directory: "/" - schedule: - interval: daily - time: "06:45" - timezone: Asia/Dacca - open-pull-requests-limit: 10 - target-branch: develop + - package-ecosystem: nuget + directory: "/" + schedule: + interval: weekly + time: "06:45" + timezone: Asia/Dacca + open-pull-requests-limit: 5 + target-branch: develop diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d3620988..e494193b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ jobs: - os: ubuntu-latest solution: Simplify.NetCore.slnx tests-exclude-category: Windows - test-framework-command: --framework net9.0 + test-framework-command: --framework net10.0 env: DOTNET_TEST_FRAMEWORK: ${{ matrix.dotnet-test-framework }} # Set at job level @@ -32,7 +32,7 @@ jobs: - name: Install DotNet uses: actions/setup-dotnet@v5 with: - dotnet-version: "9.0.x" + dotnet-version: "10.0.x" - name: Restore Dependencies run: dotnet restore ${{ matrix.solution }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 81634af6..dec87faa 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,14 +1,3 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# name: "CodeQL" on: @@ -22,7 +11,7 @@ on: jobs: analyze: name: Analyze - runs-on: windows-latest + runs-on: ubuntu-latest permissions: actions: read contents: read @@ -32,31 +21,16 @@ jobs: fail-fast: false matrix: language: ["csharp"] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - # Learn more: - # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed steps: - name: Checkout repository uses: actions/checkout@v6 - # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - - name: Setup .NET - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 9.0.x - - # Build - - run: dotnet build src/Simplify.slnx -v minimal + build-mode: none - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 diff --git a/.vscode/launch.json b/.vscode/launch.json index a4cfb135..a2505811 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,8 +6,8 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "Build Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester", - "program": "${workspaceFolder}/src/Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester/bin/Debug/net9.0/Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester.dll", - "cwd": "${workspaceFolder}/src/Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester/bin/Debug/net9.0/", + "program": "${workspaceFolder}/src/Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester/bin/Debug/net10.0/Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester.dll", + "cwd": "${workspaceFolder}/src/Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester/bin/Debug/net10.0/", "internalConsoleOptions": "openOnSessionStart", "serverReadyAction": { "action": "openExternally", @@ -23,8 +23,8 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "Build Simplify.Examples.Repository.EntityFramework.App", - "program": "${workspaceFolder}/src/Examples/Simplify.Examples.Repository.EntityFramework.App/bin/Debug/net9.0/Simplify.Examples.Repository.EntityFramework.App.dll", - "cwd": "${workspaceFolder}/src/Examples/Simplify.Examples.Repository.EntityFramework.App/bin/Debug/net9.0/", + "program": "${workspaceFolder}/src/Examples/Simplify.Examples.Repository.EntityFramework.App/bin/Debug/net10.0/Simplify.Examples.Repository.EntityFramework.App.dll", + "cwd": "${workspaceFolder}/src/Examples/Simplify.Examples.Repository.EntityFramework.App/bin/Debug/net10.0/", "internalConsoleOptions": "openOnSessionStart", "args": [ "TestUser" @@ -38,8 +38,8 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "Build Simplify.Scheduler.IntegrationTester", - "program": "${workspaceFolder}/src/Simplify.Scheduler.IntegrationTester/bin/Debug/net9.0/Simplify.Scheduler.IntegrationTester.dll", - "cwd": "${workspaceFolder}/src/Simplify.Scheduler.IntegrationTester/bin/Debug/net9.0/", + "program": "${workspaceFolder}/src/Simplify.Scheduler.IntegrationTester/bin/Debug/net10.0/Simplify.Scheduler.IntegrationTester.dll", + "cwd": "${workspaceFolder}/src/Simplify.Scheduler.IntegrationTester/bin/Debug/net10.0/", "env": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/.vscode/settings.json b/.vscode/settings.json index e43b4ba1..181191ba 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,7 @@ "dotnet-test-explorer.testArguments": "/p:CollectCoverage=true /p:CoverletOutputFormat=lcov /p:CoverletOutput=./coverage/lcov.info", "cSpell.words": [ "appsettings", + "ASPNETCORE", "Autofac", "Cacheable", "cref", @@ -13,10 +14,12 @@ "langword", "Linq", "localizable", + "nameof", "ncrontab", "netstandard", "Postgre", "registrator", + "resx", "seealso", "serializer", "SMTP", diff --git a/src/.editorconfig b/src/.editorconfig deleted file mode 100644 index 36a0aa4a..00000000 --- a/src/.editorconfig +++ /dev/null @@ -1,322 +0,0 @@ - -[*] - -# Microsoft .NET properties -csharp_indent_braces=false -csharp_indent_switch_labels=true -csharp_new_line_before_catch=true -csharp_new_line_before_else=true -csharp_new_line_before_finally=true -csharp_new_line_before_members_in_object_initializers=false -csharp_new_line_before_open_brace=all -csharp_new_line_between_query_expression_clauses=true -csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion -csharp_preserve_single_line_blocks=true -csharp_space_after_cast=false -csharp_space_after_colon_in_inheritance_clause=true -csharp_space_after_comma=true -csharp_space_after_dot=false -csharp_space_after_keywords_in_control_flow_statements=true -csharp_space_after_semicolon_in_for_statement=true -csharp_space_around_binary_operators=before_and_after -csharp_space_before_colon_in_inheritance_clause=true -csharp_space_before_comma=false -csharp_space_before_dot=false -csharp_space_before_open_square_brackets=false -csharp_space_before_semicolon_in_for_statement=false -csharp_space_between_empty_square_brackets=false -csharp_space_between_method_call_empty_parameter_list_parentheses=false -csharp_space_between_method_call_name_and_opening_parenthesis=false -csharp_space_between_method_call_parameter_list_parentheses=false -csharp_space_between_method_declaration_empty_parameter_list_parentheses=false -csharp_space_between_method_declaration_name_and_open_parenthesis=false -csharp_space_between_method_declaration_parameter_list_parentheses=false -csharp_space_between_parentheses=false -csharp_space_between_square_brackets=false -csharp_style_var_elsewhere=true:suggestion -csharp_style_var_for_built_in_types=true:suggestion -csharp_style_var_when_type_is_apparent=true:suggestion -csharp_using_directive_placement=outside_namespace:silent -dotnet_diagnostic.bc40000.severity=warning -dotnet_diagnostic.bc400005.severity=warning -dotnet_diagnostic.bc40008.severity=warning -dotnet_diagnostic.bc40056.severity=warning -dotnet_diagnostic.bc42016.severity=warning -dotnet_diagnostic.bc42024.severity=warning -dotnet_diagnostic.bc42025.severity=warning -dotnet_diagnostic.bc42104.severity=warning -dotnet_diagnostic.bc42105.severity=warning -dotnet_diagnostic.bc42106.severity=warning -dotnet_diagnostic.bc42107.severity=warning -dotnet_diagnostic.bc42304.severity=warning -dotnet_diagnostic.bc42309.severity=warning -dotnet_diagnostic.bc42322.severity=warning -dotnet_diagnostic.bc42349.severity=warning -dotnet_diagnostic.bc42353.severity=warning -dotnet_diagnostic.bc42354.severity=warning -dotnet_diagnostic.bc42355.severity=warning -dotnet_diagnostic.bc42356.severity=warning -dotnet_diagnostic.bc42358.severity=warning -dotnet_diagnostic.cs0067.severity=warning -dotnet_diagnostic.cs0078.severity=warning -dotnet_diagnostic.cs0108.severity=warning -dotnet_diagnostic.cs0109.severity=warning -dotnet_diagnostic.cs0114.severity=warning -dotnet_diagnostic.cs0162.severity=warning -dotnet_diagnostic.cs0164.severity=warning -dotnet_diagnostic.cs0168.severity=warning -dotnet_diagnostic.cs0169.severity=warning -dotnet_diagnostic.cs0183.severity=warning -dotnet_diagnostic.cs0184.severity=warning -dotnet_diagnostic.cs0197.severity=warning -dotnet_diagnostic.cs0219.severity=warning -dotnet_diagnostic.cs0252.severity=warning -dotnet_diagnostic.cs0253.severity=warning -dotnet_diagnostic.cs0414.severity=warning -dotnet_diagnostic.cs0420.severity=warning -dotnet_diagnostic.cs0465.severity=warning -dotnet_diagnostic.cs0469.severity=warning -dotnet_diagnostic.cs0612.severity=warning -dotnet_diagnostic.cs0618.severity=warning -dotnet_diagnostic.cs0628.severity=warning -dotnet_diagnostic.cs0642.severity=warning -dotnet_diagnostic.cs0649.severity=warning -dotnet_diagnostic.cs0652.severity=warning -dotnet_diagnostic.cs0657.severity=warning -dotnet_diagnostic.cs0658.severity=warning -dotnet_diagnostic.cs0659.severity=warning -dotnet_diagnostic.cs0660.severity=warning -dotnet_diagnostic.cs0661.severity=warning -dotnet_diagnostic.cs0665.severity=warning -dotnet_diagnostic.cs0672.severity=warning -dotnet_diagnostic.cs0675.severity=warning -dotnet_diagnostic.cs0693.severity=warning -dotnet_diagnostic.cs1030.severity=warning -dotnet_diagnostic.cs1058.severity=warning -dotnet_diagnostic.cs1066.severity=warning -dotnet_diagnostic.cs1522.severity=warning -dotnet_diagnostic.cs1570.severity=warning -dotnet_diagnostic.cs1571.severity=warning -dotnet_diagnostic.cs1572.severity=warning -dotnet_diagnostic.cs1573.severity=warning -dotnet_diagnostic.cs1574.severity=warning -dotnet_diagnostic.cs1580.severity=warning -dotnet_diagnostic.cs1581.severity=warning -dotnet_diagnostic.cs1584.severity=warning -dotnet_diagnostic.cs1587.severity=warning -dotnet_diagnostic.cs1589.severity=warning -dotnet_diagnostic.cs1590.severity=warning -dotnet_diagnostic.cs1591.severity=warning -dotnet_diagnostic.cs1592.severity=warning -dotnet_diagnostic.cs1710.severity=warning -dotnet_diagnostic.cs1711.severity=warning -dotnet_diagnostic.cs1712.severity=warning -dotnet_diagnostic.cs1717.severity=warning -dotnet_diagnostic.cs1723.severity=warning -dotnet_diagnostic.cs1911.severity=warning -dotnet_diagnostic.cs1957.severity=warning -dotnet_diagnostic.cs1981.severity=warning -dotnet_diagnostic.cs1998.severity=warning -dotnet_diagnostic.cs4014.severity=warning -dotnet_diagnostic.cs7022.severity=warning -dotnet_diagnostic.cs7023.severity=warning -dotnet_diagnostic.cs7095.severity=warning -dotnet_diagnostic.cs8094.severity=warning -dotnet_diagnostic.cs8123.severity=warning -dotnet_diagnostic.cs8321.severity=warning -dotnet_diagnostic.cs8383.severity=warning -dotnet_diagnostic.cs8416.severity=warning -dotnet_diagnostic.cs8417.severity=warning -dotnet_diagnostic.cs8424.severity=warning -dotnet_diagnostic.cs8425.severity=warning -dotnet_diagnostic.cs8509.severity=warning -dotnet_diagnostic.cs8597.severity=warning -dotnet_diagnostic.cs8600.severity=warning -dotnet_diagnostic.cs8601.severity=warning -dotnet_diagnostic.cs8602.severity=warning -dotnet_diagnostic.cs8603.severity=warning -dotnet_diagnostic.cs8604.severity=warning -dotnet_diagnostic.cs8605.severity=warning -dotnet_diagnostic.cs8607.severity=warning -dotnet_diagnostic.cs8608.severity=warning -dotnet_diagnostic.cs8609.severity=warning -dotnet_diagnostic.cs8610.severity=warning -dotnet_diagnostic.cs8611.severity=warning -dotnet_diagnostic.cs8612.severity=warning -dotnet_diagnostic.cs8613.severity=warning -dotnet_diagnostic.cs8614.severity=warning -dotnet_diagnostic.cs8615.severity=warning -dotnet_diagnostic.cs8616.severity=warning -dotnet_diagnostic.cs8617.severity=warning -dotnet_diagnostic.cs8618.severity=warning -dotnet_diagnostic.cs8619.severity=warning -dotnet_diagnostic.cs8620.severity=warning -dotnet_diagnostic.cs8621.severity=warning -dotnet_diagnostic.cs8622.severity=warning -dotnet_diagnostic.cs8624.severity=warning -dotnet_diagnostic.cs8625.severity=warning -dotnet_diagnostic.cs8629.severity=warning -dotnet_diagnostic.cs8631.severity=warning -dotnet_diagnostic.cs8632.severity=warning -dotnet_diagnostic.cs8633.severity=warning -dotnet_diagnostic.cs8634.severity=warning -dotnet_diagnostic.cs8643.severity=warning -dotnet_diagnostic.cs8644.severity=warning -dotnet_diagnostic.cs8645.severity=warning -dotnet_diagnostic.cs8655.severity=warning -dotnet_diagnostic.cs8656.severity=warning -dotnet_diagnostic.cs8667.severity=warning -dotnet_diagnostic.cs8669.severity=warning -dotnet_diagnostic.cs8670.severity=warning -dotnet_diagnostic.cs8714.severity=warning -dotnet_diagnostic.cs8762.severity=warning -dotnet_diagnostic.cs8763.severity=warning -dotnet_diagnostic.cs8764.severity=warning -dotnet_diagnostic.cs8765.severity=warning -dotnet_diagnostic.cs8766.severity=warning -dotnet_diagnostic.cs8767.severity=warning -dotnet_diagnostic.cs8768.severity=warning -dotnet_diagnostic.cs8769.severity=warning -dotnet_diagnostic.cs8770.severity=warning -dotnet_diagnostic.cs8774.severity=warning -dotnet_diagnostic.cs8775.severity=warning -dotnet_diagnostic.cs8776.severity=warning -dotnet_diagnostic.cs8777.severity=warning -dotnet_diagnostic.cs8794.severity=warning -dotnet_diagnostic.cs8819.severity=warning -dotnet_diagnostic.cs8824.severity=warning -dotnet_diagnostic.cs8825.severity=warning -dotnet_diagnostic.cs8851.severity=warning -dotnet_diagnostic.cs8860.severity=warning -dotnet_diagnostic.cs8892.severity=warning -dotnet_diagnostic.cs8907.severity=warning -dotnet_diagnostic.wme006.severity=warning -dotnet_naming_rule.constants_rule.import_to_resharper=as_predefined -dotnet_naming_rule.constants_rule.severity=warning -dotnet_naming_rule.constants_rule.style=upper_camel_case_style -dotnet_naming_rule.constants_rule.symbols=constants_symbols -dotnet_naming_rule.event_rule.import_to_resharper=as_predefined -dotnet_naming_rule.event_rule.severity=warning -dotnet_naming_rule.event_rule.style=upper_camel_case_style -dotnet_naming_rule.event_rule.symbols=event_symbols -dotnet_naming_rule.interfaces_rule.import_to_resharper=as_predefined -dotnet_naming_rule.interfaces_rule.severity=warning -dotnet_naming_rule.interfaces_rule.style=i_upper_camel_case_style -dotnet_naming_rule.interfaces_rule.symbols=interfaces_symbols -dotnet_naming_rule.locals_rule.import_to_resharper=as_predefined -dotnet_naming_rule.locals_rule.severity=warning -dotnet_naming_rule.locals_rule.style=lower_camel_case_style_1 -dotnet_naming_rule.locals_rule.symbols=locals_symbols -dotnet_naming_rule.local_constants_rule.import_to_resharper=as_predefined -dotnet_naming_rule.local_constants_rule.severity=warning -dotnet_naming_rule.local_constants_rule.style=lower_camel_case_style_1 -dotnet_naming_rule.local_constants_rule.symbols=local_constants_symbols -dotnet_naming_rule.local_functions_rule.import_to_resharper=as_predefined -dotnet_naming_rule.local_functions_rule.severity=warning -dotnet_naming_rule.local_functions_rule.style=upper_camel_case_style -dotnet_naming_rule.local_functions_rule.symbols=local_functions_symbols -dotnet_naming_rule.method_rule.import_to_resharper=as_predefined -dotnet_naming_rule.method_rule.severity=warning -dotnet_naming_rule.method_rule.style=upper_camel_case_style -dotnet_naming_rule.method_rule.symbols=method_symbols -dotnet_naming_rule.parameters_rule.import_to_resharper=as_predefined -dotnet_naming_rule.parameters_rule.severity=warning -dotnet_naming_rule.parameters_rule.style=lower_camel_case_style_1 -dotnet_naming_rule.parameters_rule.symbols=parameters_symbols -dotnet_naming_rule.private_constants_rule.import_to_resharper=as_predefined -dotnet_naming_rule.private_constants_rule.severity=warning -dotnet_naming_rule.private_constants_rule.style=upper_camel_case_style -dotnet_naming_rule.private_constants_rule.symbols=private_constants_symbols -dotnet_naming_rule.private_instance_fields_rule.import_to_resharper=as_predefined -dotnet_naming_rule.private_instance_fields_rule.severity=warning -dotnet_naming_rule.private_instance_fields_rule.style=lower_camel_case_style -dotnet_naming_rule.private_instance_fields_rule.symbols=private_instance_fields_symbols -dotnet_naming_rule.private_static_fields_rule.import_to_resharper=as_predefined -dotnet_naming_rule.private_static_fields_rule.severity=warning -dotnet_naming_rule.private_static_fields_rule.style=lower_camel_case_style -dotnet_naming_rule.private_static_fields_rule.symbols=private_static_fields_symbols -dotnet_naming_rule.private_static_readonly_rule.import_to_resharper=as_predefined -dotnet_naming_rule.private_static_readonly_rule.severity=warning -dotnet_naming_rule.private_static_readonly_rule.style=upper_camel_case_style -dotnet_naming_rule.private_static_readonly_rule.symbols=private_static_readonly_symbols -dotnet_naming_rule.property_rule.import_to_resharper=as_predefined -dotnet_naming_rule.property_rule.severity=warning -dotnet_naming_rule.property_rule.style=upper_camel_case_style -dotnet_naming_rule.property_rule.symbols=property_symbols -dotnet_naming_rule.public_fields_rule.import_to_resharper=as_predefined -dotnet_naming_rule.public_fields_rule.severity=warning -dotnet_naming_rule.public_fields_rule.style=upper_camel_case_style -dotnet_naming_rule.public_fields_rule.symbols=public_fields_symbols -dotnet_naming_rule.static_readonly_rule.import_to_resharper=as_predefined -dotnet_naming_rule.static_readonly_rule.severity=warning -dotnet_naming_rule.static_readonly_rule.style=upper_camel_case_style -dotnet_naming_rule.static_readonly_rule.symbols=static_readonly_symbols -dotnet_naming_rule.types_and_namespaces_rule.import_to_resharper=as_predefined -dotnet_naming_rule.types_and_namespaces_rule.severity=warning -dotnet_naming_rule.types_and_namespaces_rule.style=upper_camel_case_style -dotnet_naming_rule.types_and_namespaces_rule.symbols=types_and_namespaces_symbols -dotnet_naming_rule.type_parameters_rule.import_to_resharper=as_predefined -dotnet_naming_rule.type_parameters_rule.severity=warning -dotnet_naming_rule.type_parameters_rule.style=t_upper_camel_case_style -dotnet_naming_rule.type_parameters_rule.symbols=type_parameters_symbols -dotnet_naming_style.i_upper_camel_case_style.capitalization=pascal_case -dotnet_naming_style.i_upper_camel_case_style.required_prefix=I -dotnet_naming_style.lower_camel_case_style.capitalization=camel_case -dotnet_naming_style.lower_camel_case_style.required_prefix=_ -dotnet_naming_style.lower_camel_case_style_1.capitalization=camel_case -dotnet_naming_style.t_upper_camel_case_style.capitalization=pascal_case -dotnet_naming_style.t_upper_camel_case_style.required_prefix=T -dotnet_naming_style.upper_camel_case_style.capitalization=pascal_case -dotnet_naming_symbols.constants_symbols.applicable_accessibilities=public,internal,protected,protected_internal,private_protected -dotnet_naming_symbols.constants_symbols.applicable_kinds=field -dotnet_naming_symbols.constants_symbols.required_modifiers=const -dotnet_naming_symbols.event_symbols.applicable_accessibilities=* -dotnet_naming_symbols.event_symbols.applicable_kinds=event -dotnet_naming_symbols.interfaces_symbols.applicable_accessibilities=* -dotnet_naming_symbols.interfaces_symbols.applicable_kinds=interface -dotnet_naming_symbols.locals_symbols.applicable_accessibilities=* -dotnet_naming_symbols.locals_symbols.applicable_kinds=local -dotnet_naming_symbols.local_constants_symbols.applicable_accessibilities=* -dotnet_naming_symbols.local_constants_symbols.applicable_kinds=local -dotnet_naming_symbols.local_constants_symbols.required_modifiers=const -dotnet_naming_symbols.local_functions_symbols.applicable_accessibilities=* -dotnet_naming_symbols.local_functions_symbols.applicable_kinds=local_function -dotnet_naming_symbols.method_symbols.applicable_accessibilities=* -dotnet_naming_symbols.method_symbols.applicable_kinds=method -dotnet_naming_symbols.parameters_symbols.applicable_accessibilities=* -dotnet_naming_symbols.parameters_symbols.applicable_kinds=parameter -dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities=private -dotnet_naming_symbols.private_constants_symbols.applicable_kinds=field -dotnet_naming_symbols.private_constants_symbols.required_modifiers=const -dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities=private -dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds=field -dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities=private -dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds=field -dotnet_naming_symbols.private_static_fields_symbols.required_modifiers=static -dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities=private -dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds=field -dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers=static,readonly -dotnet_naming_symbols.property_symbols.applicable_accessibilities=* -dotnet_naming_symbols.property_symbols.applicable_kinds=property -dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities=public,internal,protected,protected_internal,private_protected -dotnet_naming_symbols.public_fields_symbols.applicable_kinds=field -dotnet_naming_symbols.static_readonly_symbols.applicable_accessibilities=public,internal,protected,protected_internal,private_protected -dotnet_naming_symbols.static_readonly_symbols.applicable_kinds=field -dotnet_naming_symbols.static_readonly_symbols.required_modifiers=static,readonly -dotnet_naming_symbols.types_and_namespaces_symbols.applicable_accessibilities=* -dotnet_naming_symbols.types_and_namespaces_symbols.applicable_kinds=namespace,class,struct,enum,delegate -dotnet_naming_symbols.type_parameters_symbols.applicable_accessibilities=* -dotnet_naming_symbols.type_parameters_symbols.applicable_kinds=type_parameter -dotnet_separate_import_directive_groups=false -dotnet_sort_system_directives_first=true -dotnet_style_parentheses_in_arithmetic_binary_operators=never_if_unnecessary:none -dotnet_style_parentheses_in_other_binary_operators=never_if_unnecessary:none -dotnet_style_parentheses_in_relational_binary_operators=never_if_unnecessary:none -dotnet_style_predefined_type_for_locals_parameters_members=true:suggestion -dotnet_style_predefined_type_for_member_access=true:suggestion -dotnet_style_qualification_for_event=false:suggestion -dotnet_style_qualification_for_field=false:suggestion -dotnet_style_qualification_for_method=false:suggestion -dotnet_style_qualification_for_property=false:suggestion -dotnet_style_require_accessibility_modifiers=for_non_interface_members:suggestion -file_header_template= diff --git a/src/.nuget/NuGet.exe b/src/.nuget/NuGet.exe index ad35fa4b..6c80f449 100644 Binary files a/src/.nuget/NuGet.exe and b/src/.nuget/NuGet.exe differ diff --git a/src/Examples/Simplify.Examples.Repository.Domain/Simplify.Examples.Repository.Domain.csproj b/src/Examples/Simplify.Examples.Repository.Domain/Simplify.Examples.Repository.Domain.csproj index dc5f4468..c187ef20 100644 --- a/src/Examples/Simplify.Examples.Repository.Domain/Simplify.Examples.Repository.Domain.csproj +++ b/src/Examples/Simplify.Examples.Repository.Domain/Simplify.Examples.Repository.Domain.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 latest false diff --git a/src/Examples/Simplify.Examples.Repository.EntityFramework.App/Simplify.Examples.Repository.EntityFramework.App.csproj b/src/Examples/Simplify.Examples.Repository.EntityFramework.App/Simplify.Examples.Repository.EntityFramework.App.csproj index 6b504b10..93f385e1 100644 --- a/src/Examples/Simplify.Examples.Repository.EntityFramework.App/Simplify.Examples.Repository.EntityFramework.App.csproj +++ b/src/Examples/Simplify.Examples.Repository.EntityFramework.App/Simplify.Examples.Repository.EntityFramework.App.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 false Exe Simplify.EntityFramework, Simplify.Repository, Simplify.Repository.EntityFramework minimal example application @@ -13,10 +13,10 @@ - + - + diff --git a/src/Examples/Simplify.Examples.Repository.EntityFramework/Simplify.Examples.Repository.EntityFramework.csproj b/src/Examples/Simplify.Examples.Repository.EntityFramework/Simplify.Examples.Repository.EntityFramework.csproj index 8fc83cd4..23a674a5 100644 --- a/src/Examples/Simplify.Examples.Repository.EntityFramework/Simplify.Examples.Repository.EntityFramework.csproj +++ b/src/Examples/Simplify.Examples.Repository.EntityFramework/Simplify.Examples.Repository.EntityFramework.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 false Simplify.EntityFramework database mappings examples @@ -9,7 +9,7 @@ Licensed under LGPL - + diff --git a/src/Examples/Simplify.Examples.Repository.FluentNHibernate.App/Program.cs b/src/Examples/Simplify.Examples.Repository.FluentNHibernate.App/Program.cs index 7038b0e6..784c4610 100644 --- a/src/Examples/Simplify.Examples.Repository.FluentNHibernate.App/Program.cs +++ b/src/Examples/Simplify.Examples.Repository.FluentNHibernate.App/Program.cs @@ -3,7 +3,6 @@ using Simplify.Examples.Repository.FluentNHibernate.App.Infrastructure; using Simplify.Examples.Repository.FluentNHibernate.App.Setup; - DIContainer.Current .RegisterAll() .Verify(); diff --git a/src/Examples/Simplify.Examples.Repository.FluentNHibernate.App/Simplify.Examples.Repository.FluentNHibernate.App.csproj b/src/Examples/Simplify.Examples.Repository.FluentNHibernate.App/Simplify.Examples.Repository.FluentNHibernate.App.csproj index 44633e90..e7d91630 100644 --- a/src/Examples/Simplify.Examples.Repository.FluentNHibernate.App/Simplify.Examples.Repository.FluentNHibernate.App.csproj +++ b/src/Examples/Simplify.Examples.Repository.FluentNHibernate.App/Simplify.Examples.Repository.FluentNHibernate.App.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 false Exe @@ -14,9 +14,9 @@ - + - + diff --git a/src/Examples/Simplify.Examples.Repository.FluentNHibernate.SchemaUpdater/Simplify.Examples.Repository.FluentNHibernate.SchemaUpdater.csproj b/src/Examples/Simplify.Examples.Repository.FluentNHibernate.SchemaUpdater/Simplify.Examples.Repository.FluentNHibernate.SchemaUpdater.csproj index df01e7c8..97bc3c7d 100644 --- a/src/Examples/Simplify.Examples.Repository.FluentNHibernate.SchemaUpdater/Simplify.Examples.Repository.FluentNHibernate.SchemaUpdater.csproj +++ b/src/Examples/Simplify.Examples.Repository.FluentNHibernate.SchemaUpdater/Simplify.Examples.Repository.FluentNHibernate.SchemaUpdater.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 false Simplify.FluentNHibernate.Database integration tests examples @@ -9,7 +9,7 @@ Licensed under LGPL - + diff --git a/src/Examples/Simplify.Examples.Repository.FluentNHibernate/Simplify.Examples.Repository.FluentNHibernate.csproj b/src/Examples/Simplify.Examples.Repository.FluentNHibernate/Simplify.Examples.Repository.FluentNHibernate.csproj index 4bbdc56b..e0a83a2e 100644 --- a/src/Examples/Simplify.Examples.Repository.FluentNHibernate/Simplify.Examples.Repository.FluentNHibernate.csproj +++ b/src/Examples/Simplify.Examples.Repository.FluentNHibernate/Simplify.Examples.Repository.FluentNHibernate.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 false Simplify.FluentNHibernate database mappings examples diff --git a/src/Examples/Simplify.Scheduler.SimpleApp/Simplify.Scheduler.SimpleApp.csproj b/src/Examples/Simplify.Scheduler.SimpleApp/Simplify.Scheduler.SimpleApp.csproj index 281e9dbf..04446c0c 100644 --- a/src/Examples/Simplify.Scheduler.SimpleApp/Simplify.Scheduler.SimpleApp.csproj +++ b/src/Examples/Simplify.Scheduler.SimpleApp/Simplify.Scheduler.SimpleApp.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 Exe false @@ -12,7 +12,7 @@ Licensed under LGPL - + diff --git a/src/Simplify.Bus.Impl.Tests/Simplify.Bus.Impl.Tests.csproj b/src/Simplify.Bus.Impl.Tests/Simplify.Bus.Impl.Tests.csproj index 14666ab9..6c3c8d54 100644 --- a/src/Simplify.Bus.Impl.Tests/Simplify.Bus.Impl.Tests.csproj +++ b/src/Simplify.Bus.Impl.Tests/Simplify.Bus.Impl.Tests.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 enable Simplify.Bus.Impl unit tests diff --git a/src/Simplify.Bus.Impl.UsabilityTests/Simplify.Bus.Impl.UsabilityTests.csproj b/src/Simplify.Bus.Impl.UsabilityTests/Simplify.Bus.Impl.UsabilityTests.csproj index e0c0db7b..cdec84c1 100644 --- a/src/Simplify.Bus.Impl.UsabilityTests/Simplify.Bus.Impl.UsabilityTests.csproj +++ b/src/Simplify.Bus.Impl.UsabilityTests/Simplify.Bus.Impl.UsabilityTests.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 enable Simplify.Bus.Impl unit tests with real structure and DI diff --git a/src/Simplify.Bus.Impl/CHANGELOG.md b/src/Simplify.Bus.Impl/CHANGELOG.md index 79984fe6..57f88daa 100644 --- a/src/Simplify.Bus.Impl/CHANGELOG.md +++ b/src/Simplify.Bus.Impl/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [0.1.3] - 2026-06-25 + +### Added + +- Explicit .NET 10 Support + +### Fixed + +- `EventBusAsync` with the `ParallelWhenAll` strategy rethrew only the first handler exception (the default `await Task.WhenAll` behavior), silently losing the rest; when more than one handler fails the full `AggregateException` is now thrown + ## [0.1.2] - 2025-10-10 ### Dependencies diff --git a/src/Simplify.Bus.Impl/EventBusAsync`1.cs b/src/Simplify.Bus.Impl/EventBusAsync`1.cs index 737019f5..1b579fe1 100644 --- a/src/Simplify.Bus.Impl/EventBusAsync`1.cs +++ b/src/Simplify.Bus.Impl/EventBusAsync`1.cs @@ -4,23 +4,32 @@ namespace Simplify.Bus.Impl; -public class EventBusAsync : IEventBusAsync +public class EventBusAsync(IReadOnlyCollection> eventHandlers, PublishStrategy publishStrategy = PublishStrategy.SyncStopOnException) : IEventBusAsync { - private readonly IReadOnlyCollection> _eventHandlers; - private readonly PublishStrategy _publishStrategy; - - public EventBusAsync(IReadOnlyCollection> eventHandlers, PublishStrategy publishStrategy = PublishStrategy.SyncStopOnException) - { - _eventHandlers = eventHandlers ?? throw new System.ArgumentNullException(nameof(eventHandlers)); - _publishStrategy = publishStrategy; - } + private readonly IReadOnlyCollection> _eventHandlers = eventHandlers ?? throw new System.ArgumentNullException(nameof(eventHandlers)); public async Task Publish(TEvent busEvent) { - if (_publishStrategy == PublishStrategy.SyncStopOnException) + if (publishStrategy == PublishStrategy.SyncStopOnException) foreach (var item in _eventHandlers) await item.Handle(busEvent); - else if (_publishStrategy == PublishStrategy.ParallelWhenAll) - await Task.WhenAll(_eventHandlers.Select(x => x.Handle(busEvent)).ToArray()); + else if (publishStrategy == PublishStrategy.ParallelWhenAll) + { + var whenAllTask = Task.WhenAll([.. _eventHandlers.Select(x => x.Handle(busEvent))]); + + try + { + await whenAllTask; + } + catch + { + // Awaiting a faulted Task.WhenAll rethrows only the first exception; when more than one + // handler failed, surface all of them so no handler error is silently lost. + if (whenAllTask.Exception is { InnerExceptions.Count: > 1 }) + throw whenAllTask.Exception; + + throw; + } + } } } \ No newline at end of file diff --git a/src/Simplify.Bus.Impl/Simplify.Bus.Impl.csproj b/src/Simplify.Bus.Impl/Simplify.Bus.Impl.csproj index 5e489393..87dccec5 100644 --- a/src/Simplify.Bus.Impl/Simplify.Bus.Impl.csproj +++ b/src/Simplify.Bus.Impl/Simplify.Bus.Impl.csproj @@ -1,6 +1,6 @@  - net6.0;netstandard2.0 + net10.0;net6.0;netstandard2.0 latest enable true @@ -8,7 +8,7 @@ snupkg false - 0.1.2 + 0.1.3 Simplify.Bus implementation Simplify diff --git a/src/Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester/Program.cs b/src/Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester/Program.cs index 2a2bd6f4..7b670a6b 100644 --- a/src/Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester/Program.cs +++ b/src/Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester/Program.cs @@ -1,13 +1,45 @@ -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; +using DryIoc; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Hosting; +using Simplify.DI; +using Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester; +using Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester.Setup; +using Simplify.DI.Provider.DryIoc; -namespace Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester; +var builder = WebApplication.CreateBuilder(args); -public class Program +// Set up the Simplify.DI (DryIoc) container + +DIContainer.Current = new DryIocDIProvider { - public static void Main(string[] args) => CreateWebHostBuilder(args).Build().Run(); + Container = new Container() + .With(rules => + rules.With(FactoryMethod.ConstructorWithResolvableArguments) + .WithoutThrowOnRegisteringDisposableTransient()) +}; + +// Plug Simplify.DI into the host so the framework resolves everything through it + +builder.Host.UseServiceProviderFactory(new SimplifyDIServiceProviderFactory()); + +// Registrations using Simplify.DI + +DIContainer.Current.RegisterAll(); + +// Unresolved types fix + +DIContainer.Current.Register(LifetimeType.Singleton); + +// Registrations using Microsoft.Extensions.DependencyInjection + +builder.Services.RegisterAll(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) + app.UseDeveloperExceptionPage(); + +app.Run(context => context.Response.WriteAsync("Hello World!")); - public static IWebHostBuilder CreateWebHostBuilder(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseStartup(); -} \ No newline at end of file +await app.RunAsync(); diff --git a/src/Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester/Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester.csproj b/src/Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester/Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester.csproj index f64e8325..7a732677 100644 --- a/src/Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester/Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester.csproj +++ b/src/Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester/Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester.csproj @@ -1,6 +1,6 @@  - net9.0 + net10.0 diff --git a/src/Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester/SimplifyDIServiceProviderFactory.cs b/src/Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester/SimplifyDIServiceProviderFactory.cs new file mode 100644 index 00000000..f215231a --- /dev/null +++ b/src/Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester/SimplifyDIServiceProviderFactory.cs @@ -0,0 +1,17 @@ +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester; + +/// +/// Plugs Simplify.DI into the .NET generic host as a custom container. +/// This is the modern, supported replacement for the deprecated WebHost/Startup pattern where +/// Startup.ConfigureServices returned an (rejected by the generic host). +/// +public class SimplifyDIServiceProviderFactory : IServiceProviderFactory +{ + public IServiceCollection CreateBuilder(IServiceCollection services) => services; + + public IServiceProvider CreateServiceProvider(IServiceCollection containerBuilder) => + DIContainer.Current.IntegrateWithMicrosoftDependencyInjectionAndVerify(containerBuilder); +} diff --git a/src/Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester/Startup.cs b/src/Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester/Startup.cs deleted file mode 100644 index 8f33d286..00000000 --- a/src/Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester/Startup.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using DryIoc; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester.Setup; -using Simplify.DI.Provider.DryIoc; - -namespace Simplify.DI.Integration.Microsoft.Extensions.DependencyInjection.Tester; - -public class Startup -{ - public IServiceProvider ConfigureServices(IServiceCollection services) - { - // DryIoc specific workaround - - var container = new DryIocDIProvider - { - Container = new Container() - .With(rules => - rules.With(FactoryMethod.ConstructorWithResolvableArguments) - .WithoutThrowOnRegisteringDisposableTransient()) - }; - - DIContainer.Current = container; - - // Registrations using Microsoft.Extensions.DependencyInjection - services.RegisterAll(); - - // Registrations using Simplify.DI - DIContainer.Current.RegisterAll(); - - // Unresolved types fix - - DIContainer.Current.Register(LifetimeType.Singleton); - - return DIContainer.Current.IntegrateWithMicrosoftDependencyInjectionAndVerify(services); - // return DIContainer.Current.IntegrateWithMicrosoftDependencyInjection(services); - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - app.UseDeveloperExceptionPage(); - - app.Run(x => x.Response.WriteAsync("Hello World!")); - } -} \ No newline at end of file diff --git a/src/Simplify.DI.Provider.Autofac.Tests/Simplify.DI.Provider.Autofac.Tests.csproj b/src/Simplify.DI.Provider.Autofac.Tests/Simplify.DI.Provider.Autofac.Tests.csproj index 580d3561..6b6f31b4 100644 --- a/src/Simplify.DI.Provider.Autofac.Tests/Simplify.DI.Provider.Autofac.Tests.csproj +++ b/src/Simplify.DI.Provider.Autofac.Tests/Simplify.DI.Provider.Autofac.Tests.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 Simplify.DI.Provider.Autofac unit tests Simplify diff --git a/src/Simplify.DI.Provider.CastleWindsor.Tests/Simplify.DI.Provider.CastleWindsor.Tests.csproj b/src/Simplify.DI.Provider.CastleWindsor.Tests/Simplify.DI.Provider.CastleWindsor.Tests.csproj index 180b1763..205ad8f5 100644 --- a/src/Simplify.DI.Provider.CastleWindsor.Tests/Simplify.DI.Provider.CastleWindsor.Tests.csproj +++ b/src/Simplify.DI.Provider.CastleWindsor.Tests/Simplify.DI.Provider.CastleWindsor.Tests.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 Simplify.DI.Provider.CastleWindsor unit tests Simplify diff --git a/src/Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection.Tests/Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection.Tests.csproj b/src/Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection.Tests/Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection.Tests.csproj index 67704993..ed7db68d 100644 --- a/src/Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection.Tests/Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection.Tests.csproj +++ b/src/Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection.Tests/Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection.Tests.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection unit tests Simplify diff --git a/src/Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection/CHANGELOG.md b/src/Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection/CHANGELOG.md index 2d3ef96b..446c1d4e 100644 --- a/src/Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection/CHANGELOG.md +++ b/src/Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [1.8.0] - 2026-06-25 + +### Fixed + +- `Register(Type, Type, ...)` defaulted to the `Singleton` lifetime while the `IDIRegistrator` contract and all other providers default to `PerLifetimeScope`, which could produce captive dependencies (a singleton capturing a scoped/transient service); the default lifetime is now `PerLifetimeScope` + +### Dependencies + +- Microsoft.Extensions.DependencyInjection bump to 10.0.9 + ## [1.7.0] - 2025-06-15 ### Removed diff --git a/src/Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection/MicrosoftDependencyInjectionDIProvider.cs b/src/Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection/MicrosoftDependencyInjectionDIProvider.cs index 0e1e2bda..5a8da84d 100644 --- a/src/Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection/MicrosoftDependencyInjectionDIProvider.cs +++ b/src/Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection/MicrosoftDependencyInjectionDIProvider.cs @@ -57,7 +57,7 @@ public IServiceProvider ServiceProvider /// Service type. /// Implementation type. /// Lifetime type of the registering services type. - public IDIRegistrator Register(Type serviceType, Type implementationType, LifetimeType lifetimeType = LifetimeType.Singleton) + public IDIRegistrator Register(Type serviceType, Type implementationType, LifetimeType lifetimeType = LifetimeType.PerLifetimeScope) { switch (lifetimeType) { diff --git a/src/Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection/Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection.csproj b/src/Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection/Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection.csproj index a61d6347..4bf2d1c5 100644 --- a/src/Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection/Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection.csproj +++ b/src/Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection/Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection.csproj @@ -8,7 +8,7 @@ snupkg true - 1.7 + 1.8.0 Simplify.DI Microsoft.Extensions.DependencyInjection provider Simplify @@ -23,7 +23,7 @@ See https://github.com/SimplifyNet/Simplify/tree/master/src/Simplify.DI.Provider.Microsoft.Extensions.DependencyInjection/CHANGELOG.md for details - + diff --git a/src/Simplify.DI.Provider.SimpleInjector.Tests/Simplify.DI.Provider.SimpleInjector.Tests.csproj b/src/Simplify.DI.Provider.SimpleInjector.Tests/Simplify.DI.Provider.SimpleInjector.Tests.csproj index fddce9c1..c163b497 100644 --- a/src/Simplify.DI.Provider.SimpleInjector.Tests/Simplify.DI.Provider.SimpleInjector.Tests.csproj +++ b/src/Simplify.DI.Provider.SimpleInjector.Tests/Simplify.DI.Provider.SimpleInjector.Tests.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 Simplify.DI.Provider.SimpleInjector unit tests Simplify diff --git a/src/Simplify.DI.Tests/DIContainerProviderTests.cs b/src/Simplify.DI.Tests/DIContainerProviderTests.cs index 55c0a56d..f836317e 100644 --- a/src/Simplify.DI.Tests/DIContainerProviderTests.cs +++ b/src/Simplify.DI.Tests/DIContainerProviderTests.cs @@ -88,7 +88,6 @@ public void ScopedResolve_ScopeRegistered_Resolved() Assert.That(foo, Is.Not.Null); } - [Test] public void ScopedResolve_ScopeDelegateRegistered_Resolved() { diff --git a/src/Simplify.DI.Tests/Simplify.DI.Tests.csproj b/src/Simplify.DI.Tests/Simplify.DI.Tests.csproj index 747e8e18..e6c80bc6 100644 --- a/src/Simplify.DI.Tests/Simplify.DI.Tests.csproj +++ b/src/Simplify.DI.Tests/Simplify.DI.Tests.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 Simplify.DI unit tests Simplify diff --git a/src/Simplify.DI.TestsTypes/Simplify.DI.TestsTypes.csproj b/src/Simplify.DI.TestsTypes/Simplify.DI.TestsTypes.csproj index b6a8b3e3..fa2a6d90 100644 --- a/src/Simplify.DI.TestsTypes/Simplify.DI.TestsTypes.csproj +++ b/src/Simplify.DI.TestsTypes/Simplify.DI.TestsTypes.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 Simplify.DI unit tests types Simplify diff --git a/src/Simplify.DI/CHANGELOG.md b/src/Simplify.DI/CHANGELOG.md index 6855eee8..691a231e 100644 --- a/src/Simplify.DI/CHANGELOG.md +++ b/src/Simplify.DI/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [4.3.0] - 2026-06-25 + +### Removed + +- .NET Explicit 6 support + +### Added + +- .NET Explicit 10 support + +### Fixed + +- `DIContainer.Current` lazy initialization was not thread-safe and could construct multiple container providers under concurrent first access; initialization is now synchronized +- `System` reference in .NET Framework 4.8 package + ## [4.2.11] - 2024-06-09 ### Dependencies diff --git a/src/Simplify.DI/DIContainer.cs b/src/Simplify.DI/DIContainer.cs index 6c4f3269..ae84fd41 100644 --- a/src/Simplify.DI/DIContainer.cs +++ b/src/Simplify.DI/DIContainer.cs @@ -8,6 +8,8 @@ namespace Simplify.DI; /// public class DIContainer { + private static readonly object Locker = new(); + private static IDIContainerProvider? _current; /// @@ -15,7 +17,15 @@ public class DIContainer /// public static IDIContainerProvider Current { - get => _current ??= new DryIocDIProvider(); + get + { + if (_current != null) + return _current; + + lock (Locker) + return _current ??= new DryIocDIProvider(); + } + set => _current = value ?? throw new ArgumentNullException(nameof(value)); } } \ No newline at end of file diff --git a/src/Simplify.DI/Simplify.DI.csproj b/src/Simplify.DI/Simplify.DI.csproj index ffc3b8fa..4d9deea6 100644 --- a/src/Simplify.DI/Simplify.DI.csproj +++ b/src/Simplify.DI/Simplify.DI.csproj @@ -1,16 +1,20 @@ - net6.0;netstandard2.1;netstandard2.0;net48 + net10.0;netstandard2.1;netstandard2.0;net48 latest enable false + + $(NoWarn);SYSLIB0051 + true true snupkg true - 4.2.11 + 4.3.0 Simplify DI common interface for IOC containers Simplify @@ -28,7 +32,7 @@ - + diff --git a/src/Simplify.EntityFramework.Tests/Simplify.EntityFramework.Tests.csproj b/src/Simplify.EntityFramework.Tests/Simplify.EntityFramework.Tests.csproj index 60723e8b..91d0a2e9 100644 --- a/src/Simplify.EntityFramework.Tests/Simplify.EntityFramework.Tests.csproj +++ b/src/Simplify.EntityFramework.Tests/Simplify.EntityFramework.Tests.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 latest Simplify.EntityFramework.Tests unit tests diff --git a/src/Simplify.FluentNHibernate.Tests/Simplify.FluentNHibernate.Tests.csproj b/src/Simplify.FluentNHibernate.Tests/Simplify.FluentNHibernate.Tests.csproj index 07f51555..14fb75f6 100644 --- a/src/Simplify.FluentNHibernate.Tests/Simplify.FluentNHibernate.Tests.csproj +++ b/src/Simplify.FluentNHibernate.Tests/Simplify.FluentNHibernate.Tests.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 latest Simplify.FluentNHibernate unit tests diff --git a/src/Simplify.FluentNHibernate/CHANGELOG.md b/src/Simplify.FluentNHibernate/CHANGELOG.md index 8f2013dc..582e1be1 100644 --- a/src/Simplify.FluentNHibernate/CHANGELOG.md +++ b/src/Simplify.FluentNHibernate/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [3.3.1] - 2026-06-25 + +### Added + +- .NET 10 support + +### Fixed + +- Moved `PostgreSql83ZDialect` from the leaked `Pas.Database.Session.Dialects` namespace to `Simplify.FluentNHibernate.Dialects` to match its location and the other dialects, and removed the dangling `using` +- `FluentConfigurationExtension.ExportSchema`/`UpdateSchema` did not dispose the temporary `ISessionFactory` (leaking it) and called `tx.Rollback()` unconditionally in the catch block; the session factory is now disposed and rollback only runs when the transaction is still active + ## [3.3.0] - 2025-06-15 ### Removed diff --git a/src/Simplify.FluentNHibernate/ConfigurationExtensions.cs b/src/Simplify.FluentNHibernate/ConfigurationExtensions.cs index 2320a862..8626ad74 100644 --- a/src/Simplify.FluentNHibernate/ConfigurationExtensions.cs +++ b/src/Simplify.FluentNHibernate/ConfigurationExtensions.cs @@ -4,7 +4,6 @@ using FluentNHibernate.Conventions; using Microsoft.Extensions.Configuration; using NHibernate.Driver; -using Pas.Database.Session.Dialects; using Simplify.FluentNHibernate.Dialects; using Simplify.FluentNHibernate.Drivers; using Simplify.FluentNHibernate.Interceptors; diff --git a/src/Simplify.FluentNHibernate/Dialects/PostgreSql83WithUtcDialect.cs b/src/Simplify.FluentNHibernate/Dialects/PostgreSql83WithUtcDialect.cs index f0b6bcac..530789f1 100644 --- a/src/Simplify.FluentNHibernate/Dialects/PostgreSql83WithUtcDialect.cs +++ b/src/Simplify.FluentNHibernate/Dialects/PostgreSql83WithUtcDialect.cs @@ -1,7 +1,7 @@ using System.Data; using NHibernate.Dialect; -namespace Pas.Database.Session.Dialects; +namespace Simplify.FluentNHibernate.Dialects; /// /// Provides PostgreSQL dialect which uses timestamptz DateTime format diff --git a/src/Simplify.FluentNHibernate/FluentConfigurationExtension.cs b/src/Simplify.FluentNHibernate/FluentConfigurationExtension.cs index a99b8b55..7851fd69 100644 --- a/src/Simplify.FluentNHibernate/FluentConfigurationExtension.cs +++ b/src/Simplify.FluentNHibernate/FluentConfigurationExtension.cs @@ -21,7 +21,7 @@ public static void ExportSchema(this FluentConfiguration configuration, Action config = c); - var sessionFactory = configuration.BuildSessionFactory(); + using var sessionFactory = configuration.BuildSessionFactory(); using var session = sessionFactory.OpenSession(); using var tx = session.BeginTransaction(); @@ -37,7 +37,8 @@ public static void ExportSchema(this FluentConfiguration configuration, Action config = c); - var sessionFactory = configuration.BuildSessionFactory(); + using var sessionFactory = configuration.BuildSessionFactory(); using var session = sessionFactory.OpenSession(); using var tx = session.BeginTransaction(); @@ -71,7 +72,8 @@ public static void UpdateSchema(this FluentConfiguration configuration, Action - net9.0;net8.0;netstandard2.1;netstandard2.0;net48 + net10.0;net9.0;net8.0;netstandard2.1;netstandard2.0;net48 latest enable true @@ -8,7 +8,7 @@ snupkg true - 3.3 + 3.3.1 Simplifies FluentNHibernate connection configuration and allows you to write data-base queries with lambda expressions Simplify @@ -24,7 +24,7 @@ - + diff --git a/src/Simplify.IO.Tests/FileHelperTester.cs b/src/Simplify.IO.Tests/FileHelperTester.cs index acb08934..0fe1dc0f 100644 --- a/src/Simplify.IO.Tests/FileHelperTester.cs +++ b/src/Simplify.IO.Tests/FileHelperTester.cs @@ -25,6 +25,7 @@ public void IsFileLockedForRead() [Test] [Category("Windows")] + [Platform("Win")] public void IsFileNameMadeValidCorrectly() { Assert.That(FileHelper.MakeValidFileName(@"thisIsValid.txt"), Is.EqualTo("thisIsValid.txt")); diff --git a/src/Simplify.IO.Tests/Simplify.IO.Tests.csproj b/src/Simplify.IO.Tests/Simplify.IO.Tests.csproj index 856afcef..f33696fa 100644 --- a/src/Simplify.IO.Tests/Simplify.IO.Tests.csproj +++ b/src/Simplify.IO.Tests/Simplify.IO.Tests.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 Simplify.IO unit tests Simplify diff --git a/src/Simplify.IO/CHANGELOG.md b/src/Simplify.IO/CHANGELOG.md index 3de3baf8..f89fb15c 100644 --- a/src/Simplify.IO/CHANGELOG.md +++ b/src/Simplify.IO/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## [1.5.1] - 2026-06-25 + +### Added + +- .NET 10 support + +### Fixed + +- `FileHelper.IsFileLockedForRead` and `GetLastLineOfFile` now use the configurable `IFileSystem` abstraction instead of direct file access, so they honor a custom/mocked file system +- `FileHelper.GenerateFullName` now uses `Path.Combine` instead of a hardcoded `"/"` path separator for cross-platform compatibility +- `FileHelper.GenerateFullName` now resolves the base path from `AppContext.BaseDirectory` instead of `Assembly.GetCallingAssembly().Location`, which is empty in single-file/AOT deployments (files would otherwise land in the current working directory) + +### Dependencies + +- System.IO.Abstractions bump to 22.1.1 + ## [1.5.0] - 2025-06-15 ### Removed diff --git a/src/Simplify.IO/FileHelper.cs b/src/Simplify.IO/FileHelper.cs index 7d74485a..178e7762 100644 --- a/src/Simplify.IO/FileHelper.cs +++ b/src/Simplify.IO/FileHelper.cs @@ -2,7 +2,6 @@ using System.IO; using System.IO.Abstractions; using System.Linq; -using System.Reflection; namespace Simplify.IO; @@ -48,8 +47,8 @@ public static bool IsFileLockedForRead(string filePath) if (!FileSystem.File.Exists(filePath)) throw new FileNotFoundException("File not found: " + filePath); - var file = new FileInfo(filePath); - FileStream stream = null; + var file = FileSystem.FileInfo.New(filePath); + Stream stream = null; try { @@ -85,26 +84,25 @@ public static string GetLastLineOfFile(string filePath) if (!FileSystem.File.Exists(filePath)) throw new FileNotFoundException("File not found: " + filePath); - using (var sr = new StreamReader(filePath)) - { - sr.BaseStream.Seek(0, SeekOrigin.End); + using var sr = new StreamReader(FileSystem.File.OpenRead(filePath)); - long pos = -1; + sr.BaseStream.Seek(0, SeekOrigin.End); - while (sr.BaseStream.Length + pos > 0) - { - sr.BaseStream.Seek(pos, SeekOrigin.End); - var c = sr.Read(); - sr.DiscardBufferedData(); + long pos = -1; - if (c == Convert.ToInt32('\n')) - { - sr.BaseStream.Seek(pos + 1, SeekOrigin.End); - return sr.ReadToEnd(); - } + while (sr.BaseStream.Length + pos > 0) + { + sr.BaseStream.Seek(pos, SeekOrigin.End); + var c = sr.Read(); + sr.DiscardBufferedData(); - --pos; + if (c == Convert.ToInt32('\n')) + { + sr.BaseStream.Seek(pos + 1, SeekOrigin.End); + return sr.ReadToEnd(); } + + --pos; } return null; @@ -115,18 +113,16 @@ public static string GetLastLineOfFile(string filePath) /// /// /// - public static string MakeValidFileName(string name) - { - return Path.GetInvalidFileNameChars().Aggregate(name, (current, c) => current.Replace(c, '_')); - } + public static string MakeValidFileName(string name) => Path.GetInvalidFileNameChars().Aggregate(name, (current, c) => current.Replace(c, '_')); /// - /// Generates the full name of file in current directory adding calling assembly path in the start of file name. + /// Generates the full name of file in the application base directory adding its path in the start of file name. /// /// Name of the file. /// - public static string GenerateFullName(string fileName) - { - return $"{Path.GetDirectoryName(Assembly.GetCallingAssembly().Location)}/{fileName}"; - } + /// + /// Uses which, unlike an assembly location, resolves correctly in + /// single-file and AOT deployments where Assembly.Location is empty. + /// + public static string GenerateFullName(string fileName) => Path.Combine(AppContext.BaseDirectory, fileName); } \ No newline at end of file diff --git a/src/Simplify.IO/Simplify.IO.csproj b/src/Simplify.IO/Simplify.IO.csproj index be223b76..eee0989e 100644 --- a/src/Simplify.IO/Simplify.IO.csproj +++ b/src/Simplify.IO/Simplify.IO.csproj @@ -1,6 +1,6 @@ - net9.0;netstandard2.1;netstandard2.0;net48 + net10.0;net9.0;netstandard2.1;netstandard2.0;net48 latest true @@ -8,7 +8,7 @@ snupkg true - 1.5 + 1.5.1 IO helper functions Simplify @@ -23,7 +23,7 @@ See https://github.com/SimplifyNet/Simplify/tree/master/src/Simplify.IO/CHANGELOG.md for details - + diff --git a/src/Simplify.Log/CHANGELOG.md b/src/Simplify.Log/CHANGELOG.md index ef959e72..f1406261 100644 --- a/src/Simplify.Log/CHANGELOG.md +++ b/src/Simplify.Log/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [2.3.1] - 2026-06-25 + +### Fixed + +- `Logger.GetInnerExceptionData` returned `null` which was string-interpolated as `"null"`; now returns `string.Empty` +- `Logger.Initialize` now guards against null `Assembly.GetCallingAssembly().Location` (can be null for merged/dynamic assemblies) +- `Logger.Initialize` now uses `Path.Combine` instead of hardcoded `"/"` for cross-platform path construction +- `ConfigurationBasedLoggerSettings` `PathType` parsing now uses `Enum.TryParse` instead of `Enum.Parse` to avoid crashing on invalid configuration values + +### Dependencies + +- System.IO.Abstractions bump to 22.1.1 + ## [2.3.0] - 2025-06-15 ### Removed diff --git a/src/Simplify.Log/Logger.cs b/src/Simplify.Log/Logger.cs index 1d1e69e2..73f33e5a 100644 --- a/src/Simplify.Log/Logger.cs +++ b/src/Simplify.Log/Logger.cs @@ -166,25 +166,21 @@ public string Generate(Exception e, bool addTimeInformation = true) /// Exception to get data from /// Adds time information prefix to the generated message. /// - public string GenerateWeb(Exception e, bool addTimeInformation = true) - { - return Generate(e, addTimeInformation).Replace("\r\n", "
"); - } + public string GenerateWeb(Exception e, bool addTimeInformation = true) => + Generate(e, addTimeInformation).Replace("\r\n", "
"); - private static string AddTimeInformation(string message) - { - return $"[{DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss:fff", CultureInfo.InvariantCulture)}]{message}"; - } + private static string AddTimeInformation(string message) => + $"[{DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss:fff", CultureInfo.InvariantCulture)}]{message}"; private static string GetInnerExceptionData(int currentLevel, Exception e) { if (e == null) - return null; + return string.Empty; var trace = new StackTrace(e, true); if (trace.FrameCount == 0) - return null; + return string.Empty; var fileLineNumber = trace.GetFrame(0).GetFileLineNumber(); var fileColumnNumber = trace.GetFrame(0).GetFileColumnNumber(); @@ -197,7 +193,7 @@ private static string GetInnerExceptionData(int currentLevel, Exception e) private void Initialize() => _currentLogFileName = Settings.PathType == LoggerPathType.FullPath ? Settings.FileName - : $"{Path.GetDirectoryName(Assembly.GetCallingAssembly().Location)}/{Settings.FileName}"; + : Path.Combine(Path.GetDirectoryName(Assembly.GetCallingAssembly().Location) ?? throw new InvalidOperationException("Unable to resolve calling assembly location"), Settings.FileName); private void WriteToFile(string message) { diff --git a/src/Simplify.Log/Settings/Impl/ConfigurationBasedLoggerSettings.cs b/src/Simplify.Log/Settings/Impl/ConfigurationBasedLoggerSettings.cs index c4b55f85..627e8af1 100644 --- a/src/Simplify.Log/Settings/Impl/ConfigurationBasedLoggerSettings.cs +++ b/src/Simplify.Log/Settings/Impl/ConfigurationBasedLoggerSettings.cs @@ -35,8 +35,8 @@ public ConfigurationBasedLoggerSettings(IConfiguration configuration, string con MaxFileSize = buffer; } - if (!string.IsNullOrEmpty(config["PathType"])) - PathType = (LoggerPathType)Enum.Parse(typeof(LoggerPathType), config["PathType"]); + if (!string.IsNullOrEmpty(config["PathType"]) && Enum.TryParse(config["PathType"], out var pathType)) + PathType = pathType; if (!string.IsNullOrEmpty(config["ShowTraceOutput"])) { diff --git a/src/Simplify.Log/Simplify.Log.csproj b/src/Simplify.Log/Simplify.Log.csproj index 2f08480a..f4eaf6f4 100644 --- a/src/Simplify.Log/Simplify.Log.csproj +++ b/src/Simplify.Log/Simplify.Log.csproj @@ -8,7 +8,7 @@ snupkg true - 2.3 + 2.3.1 Simple logger Simplify @@ -24,7 +24,7 @@
- + diff --git a/src/Simplify.Mail.IntegrationTests/Simplify.Mail.IntegrationTests.csproj b/src/Simplify.Mail.IntegrationTests/Simplify.Mail.IntegrationTests.csproj index 8310cb71..f94e3ad3 100644 --- a/src/Simplify.Mail.IntegrationTests/Simplify.Mail.IntegrationTests.csproj +++ b/src/Simplify.Mail.IntegrationTests/Simplify.Mail.IntegrationTests.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 latest Simplify.Mail integration tests diff --git a/src/Simplify.Mail.TestConsoleApp/Simplify.Mail.TestConsoleApp.csproj b/src/Simplify.Mail.TestConsoleApp/Simplify.Mail.TestConsoleApp.csproj index 283c9ba6..5e0df7c9 100644 --- a/src/Simplify.Mail.TestConsoleApp/Simplify.Mail.TestConsoleApp.csproj +++ b/src/Simplify.Mail.TestConsoleApp/Simplify.Mail.TestConsoleApp.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 Exe false diff --git a/src/Simplify.Mail/CHANGELOG.md b/src/Simplify.Mail/CHANGELOG.md index 8531b0fa..133a6f54 100644 --- a/src/Simplify.Mail/CHANGELOG.md +++ b/src/Simplify.Mail/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## [2.1.0] - 2026-06-25 + +### Added + +- .NET 10 support + +### Security + +- SMTP authentication no longer falls back to a plain-text connection when SSL is not explicitly enabled: STARTTLS is now negotiated opportunistically (`StartTlsWhenAvailable`) so credentials are encrypted whenever the server supports it + +### Fixed + +- `Dispose` is now idempotent and thread-safe (guarded with `Interlocked`) +- Anti-spam pool now uses `DateTime.UtcNow` to avoid mis-expiration on local clock/DST changes +- `Dispose` no longer blocks indefinitely on `_smtpLock.Wait()` when another thread holds the semaphore; uses non-blocking `Wait(0)` instead +- `ConfigurationBasedMailSenderSettings` config parsing now uses `TryParse` instead of `Parse` to avoid crashing on invalid configuration values + + +### Dependencies + +- MailKit bump to 4.17 +- Microsoft.Extensions.Configuration bump to 10.0.9 + ## [2.0.1] - 2026-04-24 ### Dependencies diff --git a/src/Simplify.Mail/MailSender.cs b/src/Simplify.Mail/MailSender.cs index c2164333..a9e873a5 100644 --- a/src/Simplify.Mail/MailSender.cs +++ b/src/Simplify.Mail/MailSender.cs @@ -18,12 +18,13 @@ namespace Simplify.Mail; public class MailSender : IMailSender, IDisposable { private static readonly object AntiSpamLocker = new(); + private static readonly Dictionary AntiSpamPool = []; private readonly SemaphoreSlim _smtpLock = new(1, 1); private SmtpClient _smtpClient; - private bool _disposed; + private int _disposed; /// /// Initializes a new instance of the class. @@ -571,26 +572,28 @@ public Task SendSeparatelyAsync(string fromMailAddress, IList addresses, /// public void Dispose() { - if (_disposed) + if (Interlocked.Exchange(ref _disposed, 1) == 1) return; - _smtpLock.Wait(); - - try + // Try to acquire the semaphore without blocking. If another thread holds it, + // that thread will clean up the SmtpClient when it finishes. + if (_smtpLock.Wait(0)) { - if (_smtpClient?.IsConnected == true) - _smtpClient.Disconnect(true); + try + { + if (_smtpClient?.IsConnected == true) + _smtpClient.Disconnect(true); - _smtpClient?.Dispose(); - _smtpClient = null; - } - finally - { - _smtpLock.Release(); + _smtpClient?.Dispose(); + _smtpClient = null; + } + finally + { + _smtpLock.Release(); + } } - _smtpLock?.Dispose(); - _disposed = true; + _smtpLock.Dispose(); GC.SuppressFinalize(this); } @@ -600,7 +603,9 @@ private async Task ConnectAsync(SmtpClient client, CancellationToken cancellatio if (client.IsConnected) return; - var secureSocketOptions = Settings.EnableSsl ? SecureSocketOptions.StartTls : SecureSocketOptions.None; + // When SSL is not explicitly enabled, still negotiate STARTTLS opportunistically if the server + // supports it, so credentials are never sent over a plain-text connection when avoidable. + var secureSocketOptions = Settings.EnableSsl ? SecureSocketOptions.StartTls : SecureSocketOptions.StartTlsWhenAvailable; await client.ConnectAsync(Settings.SmtpServerAddress, Settings.SmtpServerPortNumber, secureSocketOptions, cancellationToken); @@ -613,7 +618,9 @@ private void Connect(SmtpClient client) if (client.IsConnected) return; - var secureSocketOptions = Settings.EnableSsl ? SecureSocketOptions.StartTls : SecureSocketOptions.None; + // When SSL is not explicitly enabled, still negotiate STARTTLS opportunistically if the server + // supports it, so credentials are never sent over a plain-text connection when avoidable. + var secureSocketOptions = Settings.EnableSsl ? SecureSocketOptions.StartTls : SecureSocketOptions.StartTlsWhenAvailable; client.Connect(Settings.SmtpServerAddress, Settings.SmtpServerPortNumber, secureSocketOptions); @@ -669,7 +676,7 @@ private bool CheckAntiSpamPool(string messageBody) if (AntiSpamPool.ContainsKey(messageBody)) return true; - AntiSpamPool.Add(messageBody, DateTime.Now); + AntiSpamPool.Add(messageBody, DateTime.UtcNow); return false; } @@ -677,6 +684,6 @@ private bool CheckAntiSpamPool(string messageBody) private IList GetItemsToRemove() => [.. from item in AntiSpamPool - where (DateTime.Now - item.Value).TotalMinutes > Settings.AntiSpamPoolMessageLifeTime + where (DateTime.UtcNow - item.Value).TotalMinutes > Settings.AntiSpamPoolMessageLifeTime select item.Key]; } \ No newline at end of file diff --git a/src/Simplify.Mail/Settings/Impl/ConfigurationBasedMailSenderSettings.cs b/src/Simplify.Mail/Settings/Impl/ConfigurationBasedMailSenderSettings.cs index c777cbb2..983833e7 100644 --- a/src/Simplify.Mail/Settings/Impl/ConfigurationBasedMailSenderSettings.cs +++ b/src/Simplify.Mail/Settings/Impl/ConfigurationBasedMailSenderSettings.cs @@ -41,8 +41,8 @@ private void LoadGeneralSettings(IConfiguration config) var smtpServerPortNumberString = config["SmtpServerPortNumber"]; - if (!string.IsNullOrEmpty(smtpServerPortNumberString)) - SmtpServerPortNumber = int.Parse(smtpServerPortNumberString); + if (!string.IsNullOrEmpty(smtpServerPortNumberString) && int.TryParse(smtpServerPortNumberString, out var portNumber)) + SmtpServerPortNumber = portNumber; SmtpUserName = config["SmtpUserName"]; SmtpUserPassword = config["SmtpUserPassword"]; @@ -52,17 +52,17 @@ private void LoadExtraSettings(IConfiguration config) { var antiSpamPoolMessageLifeTimeString = config["AntiSpamPoolMessageLifeTime"]; - if (!string.IsNullOrEmpty(antiSpamPoolMessageLifeTimeString)) - AntiSpamPoolMessageLifeTime = int.Parse(antiSpamPoolMessageLifeTimeString); + if (!string.IsNullOrEmpty(antiSpamPoolMessageLifeTimeString) && int.TryParse(antiSpamPoolMessageLifeTimeString, out var lifeTime)) + AntiSpamPoolMessageLifeTime = lifeTime; var antiSpamMessagesPoolOnString = config["AntiSpamMessagesPoolOn"]; - if (!string.IsNullOrEmpty(antiSpamMessagesPoolOnString)) - AntiSpamMessagesPoolOn = bool.Parse(antiSpamMessagesPoolOnString); + if (!string.IsNullOrEmpty(antiSpamMessagesPoolOnString) && bool.TryParse(antiSpamMessagesPoolOnString, out var poolOn)) + AntiSpamMessagesPoolOn = poolOn; var enableSsl = config["EnableSsl"]; - if (!string.IsNullOrEmpty(enableSsl)) - EnableSsl = bool.Parse(enableSsl); + if (!string.IsNullOrEmpty(enableSsl) && bool.TryParse(enableSsl, out var sslEnabled)) + EnableSsl = sslEnabled; } } \ No newline at end of file diff --git a/src/Simplify.Mail/Simplify.Mail.csproj b/src/Simplify.Mail/Simplify.Mail.csproj index fdc29e2f..9a227ee9 100644 --- a/src/Simplify.Mail/Simplify.Mail.csproj +++ b/src/Simplify.Mail/Simplify.Mail.csproj @@ -1,13 +1,13 @@ - net9.0;netstandard2.1;netstandard2.0;net48 + net10.0;net9.0;netstandard2.1;netstandard2.0;net48 latest true true snupkg true - 2.0.1 + 2.1 Simple mail sender Simplify @@ -22,8 +22,8 @@ See https://github.com/SimplifyNet/Simplify/tree/master/src/Simplify.Mail/CHANGELOG.md for details - - + + diff --git a/src/Simplify.NetCore.slnx b/src/Simplify.NetCore.slnx index c544847c..6f946d9e 100644 --- a/src/Simplify.NetCore.slnx +++ b/src/Simplify.NetCore.slnx @@ -57,6 +57,7 @@ + diff --git a/src/Simplify.Pipelines/CHANGELOG.md b/src/Simplify.Pipelines/CHANGELOG.md index 435e17a7..48b311e7 100644 --- a/src/Simplify.Pipelines/CHANGELOG.md +++ b/src/Simplify.Pipelines/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [1.0.0] - 2026-06-25 + +### Breaking Change + +- Long-obsolete `ChainHandler`, `Pipeline`, `ResultingPipeline`, `PipelineProcessor`, `ValidationPipeline`, `ValidationPipelineProcessor` and their supporting types (interfaces, rules, data preparers) — use `IConveyor` / `IAsyncConveyor` instead + +### Added + +- .NET 10 support + ## [0.9.3] - 2026-05-26 ### Fixed diff --git a/src/Simplify.Pipelines/ChainHandler{T}.cs b/src/Simplify.Pipelines/ChainHandler{T}.cs deleted file mode 100644 index 9f5d4638..00000000 --- a/src/Simplify.Pipelines/ChainHandler{T}.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; - -namespace Simplify.Pipelines; - -/// -/// Provides chain of responsibility handler base class -/// -/// -[Obsolete] -public abstract class ChainHandler -{ - /// - /// Initializes a new instance of the class. - /// - /// The successor handler. - protected ChainHandler(ChainHandler successor = null) => Successor = successor; - - /// - /// Gets the successor handler. - /// - /// - /// The successor. - /// - public ChainHandler Successor { get; } - - /// - /// Executes the handler. - /// - /// The arguments. - public virtual void Execute(T args) => Successor?.Execute(args); -} \ No newline at end of file diff --git a/src/Simplify.Pipelines/IDataPreparer{T}.cs b/src/Simplify.Pipelines/IDataPreparer{T}.cs deleted file mode 100644 index 84b0a143..00000000 --- a/src/Simplify.Pipelines/IDataPreparer{T}.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; - -namespace Simplify.Pipelines; - -/// -/// Represent data preparer (retriever) for pipeline processing -/// -/// -public interface IDataPreparer -{ - /// - /// Gets the data for pipeline processing. - /// - /// - IEnumerable GetData(); -} \ No newline at end of file diff --git a/src/Simplify.Pipelines/Processing/IPipelineProcessor.cs b/src/Simplify.Pipelines/Processing/IPipelineProcessor.cs deleted file mode 100644 index 7aee0ffb..00000000 --- a/src/Simplify.Pipelines/Processing/IPipelineProcessor.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Simplify.Pipelines.Processing; - -/// -/// Represent pipeline processor -/// -[Obsolete("Please use IConveyor with exceptions")] -public interface IPipelineProcessor -{ - /// - /// Executes pipeline. - /// - void Execute(); -} \ No newline at end of file diff --git a/src/Simplify.Pipelines/Processing/IPipelineStage{T}.cs b/src/Simplify.Pipelines/Processing/IPipelineStage{T}.cs deleted file mode 100644 index 0120973d..00000000 --- a/src/Simplify.Pipelines/Processing/IPipelineStage{T}.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace Simplify.Pipelines.Processing; - -/// -/// Represent pipeline stage -/// -/// -[Obsolete("Please use IConveyor with exceptions")] -public interface IPipelineStage -{ - /// - /// Executes the stage. - /// - /// The item for execution. - /// - bool Execute(T item); -} \ No newline at end of file diff --git a/src/Simplify.Pipelines/Processing/IPipeline{T}.cs b/src/Simplify.Pipelines/Processing/IPipeline{T}.cs deleted file mode 100644 index 09c58ebd..00000000 --- a/src/Simplify.Pipelines/Processing/IPipeline{T}.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; - -namespace Simplify.Pipelines.Processing; - -/// -/// Represent pipeline -/// -/// -[Obsolete("Please use IConveyor with exceptions")] -public interface IPipeline -{ - /// - /// Occurs when pipeline is about to execute. - /// - event PipelineAction OnPipelineStart; - - /// - /// Occurs when pipeline has finished it's execution. - /// - event PipelineAction OnPipelineEnd; - - /// - /// Occurs when pipeline stage has finished it's execution. - /// - event PipelineStageAction OnStageExecuted; - - /// - /// Process item through pipeline. - /// - /// The item for execution. - /// - bool Execute(T item); -} \ No newline at end of file diff --git a/src/Simplify.Pipelines/Processing/IResultingPipelineStage{T}.cs b/src/Simplify.Pipelines/Processing/IResultingPipelineStage{T}.cs deleted file mode 100644 index b0d5b868..00000000 --- a/src/Simplify.Pipelines/Processing/IResultingPipelineStage{T}.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace Simplify.Pipelines.Processing; - -/// -/// Represent pipeline stage with processing error result information -/// -/// -/// The type of the result. -/// -[Obsolete("Please use IConveyorStage with exceptions")] -public interface IResultingPipelineStage : IPipelineStage -{ - /// - /// Gets the error result. - /// - /// - /// The error result. - /// - TResult ErrorResult { get; } -} \ No newline at end of file diff --git a/src/Simplify.Pipelines/Processing/IResultingPipeline{T}.cs b/src/Simplify.Pipelines/Processing/IResultingPipeline{T}.cs deleted file mode 100644 index 8db67586..00000000 --- a/src/Simplify.Pipelines/Processing/IResultingPipeline{T}.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; - -namespace Simplify.Pipelines.Processing; - -/// -/// Represent pipeline with processing error result information -/// -/// -/// The type of the result. -[Obsolete("Please use IConveyor with exceptions")] -public interface IResultingPipeline -{ - /// - /// Gets the error result. - /// - /// - /// The error result. - /// - TResult ErrorResult { get; } - - /// - /// Process item through pipeline. - /// - /// The item for execution. - /// - bool Execute(T item); -} \ No newline at end of file diff --git a/src/Simplify.Pipelines/Processing/PipelineAction{T}.cs b/src/Simplify.Pipelines/Processing/PipelineAction{T}.cs deleted file mode 100644 index b3a2c056..00000000 --- a/src/Simplify.Pipelines/Processing/PipelineAction{T}.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Simplify.Pipelines.Processing; - -/// -/// Provides pipeline related action delegate -/// -/// -/// The item. -[Obsolete("Please use IConveyor with exceptions")] -public delegate void PipelineAction(T item); \ No newline at end of file diff --git a/src/Simplify.Pipelines/Processing/PipelineProcessor{T}.cs b/src/Simplify.Pipelines/Processing/PipelineProcessor{T}.cs deleted file mode 100644 index 900f3783..00000000 --- a/src/Simplify.Pipelines/Processing/PipelineProcessor{T}.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; - -namespace Simplify.Pipelines.Processing; - -/// -/// Provides default pipeline processor -/// -/// -/// -[Obsolete("Please use IConveyor with exceptions")] -public class PipelineProcessor : IPipelineProcessor -{ - private readonly IPipeline _pipeline; - private readonly IDataPreparer _dataPreparer; - - /// - /// Initializes a new instance of the class. - /// - /// The pipeline. - /// The data preparer. - public PipelineProcessor(IPipeline pipeline, IDataPreparer dataPreparer) - { - _pipeline = pipeline; - _dataPreparer = dataPreparer; - } - - /// - /// Retrieve data from preparer and process it through pipeline. - /// - public virtual void Execute() - { - var data = _dataPreparer.GetData(); - - foreach (var item in data) - _pipeline.Execute(item); - } -} \ No newline at end of file diff --git a/src/Simplify.Pipelines/Processing/PipelineStageAction{T}.cs b/src/Simplify.Pipelines/Processing/PipelineStageAction{T}.cs deleted file mode 100644 index 85417549..00000000 --- a/src/Simplify.Pipelines/Processing/PipelineStageAction{T}.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace Simplify.Pipelines.Processing; - -/// -/// Provides pipeline stage action delegate -/// -/// -/// Type of the stage. -/// The item. -/// Current pipeline executing stage result. -[Obsolete("Please use IConveyor with exceptions")] -public delegate void PipelineStageAction(Type stageType, T item, bool stageResult); \ No newline at end of file diff --git a/src/Simplify.Pipelines/Processing/Pipeline{T}.cs b/src/Simplify.Pipelines/Processing/Pipeline{T}.cs deleted file mode 100644 index cc31d250..00000000 --- a/src/Simplify.Pipelines/Processing/Pipeline{T}.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Simplify.Pipelines.Processing; - -/// -/// Provides default pipeline -/// -/// -/// -[Obsolete("Please use IConveyor with exceptions")] -public class Pipeline : IPipeline -{ - private readonly IList> _stages; - - /// - /// Initializes a new instance of the class. - /// - /// The pipeline stages. - public Pipeline(IList> stages) - { - _stages = stages; - } - - /// - /// Occurs when pipeline is about to execute. - /// - public event PipelineAction OnPipelineStart; - - /// - /// Occurs when pipeline has finished it's execution. - /// - public event PipelineAction OnPipelineEnd; - - /// - /// Occurs when pipeline stage has finished it's execution. - /// - public event PipelineStageAction OnStageExecuted; - - /// - /// Process pipeline stages. - /// - /// The item for execution. - /// - public virtual bool Execute(T item) - { - OnPipelineStart?.Invoke(item); - - foreach (var stage in _stages) - { - var result = stage.Execute(item); - - OnStageExecuted?.Invoke(stage.GetType(), item, result); - - if (result == false) - return false; - } - - OnPipelineEnd?.Invoke(item); - - return true; - } -} \ No newline at end of file diff --git a/src/Simplify.Pipelines/Processing/ResultingPipeline{T}.cs b/src/Simplify.Pipelines/Processing/ResultingPipeline{T}.cs deleted file mode 100644 index 7d4306f5..00000000 --- a/src/Simplify.Pipelines/Processing/ResultingPipeline{T}.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Simplify.Pipelines.Processing; - -/// -/// Provides default resulting pipeline -/// -/// -/// The type of the result. -/// -[Obsolete("Please use IConveyor with exceptions")] -public class ResultingPipeline : IResultingPipeline -{ - private readonly IList> _stages; - - /// - /// Initializes a new instance of the class. - /// - /// The stages. - public ResultingPipeline(IList> stages) - { - _stages = stages; - } - - /// - /// Occurs when pipeline has finished it's execution. - /// - public event PipelineAction OnPipelineEnd; - - /// - /// Occurs when pipeline is about to execute. - /// - public event PipelineAction OnPipelineStart; - - /// - /// Occurs when pipeline stage has finished it's execution. - /// - public event PipelineStageAction OnStageExecuted; - - /// - /// Gets the error result. - /// - /// - /// The error result. - /// - public TResult ErrorResult { get; private set; } - - /// - /// Process pipeline stages. - /// - /// The item for execution. - /// - public bool Execute(T item) - { - OnPipelineStart?.Invoke(item); - - foreach (var stage in _stages) - { - var result = stage.Execute(item); - - OnStageExecuted?.Invoke(stage.GetType(), item, result); - - if (result) - continue; - - ErrorResult = stage.ErrorResult; - - return false; - } - - OnPipelineEnd?.Invoke(item); - - return true; - } -} \ No newline at end of file diff --git a/src/Simplify.Pipelines/ProductionLine/ConveyorExecutor{T}.cs b/src/Simplify.Pipelines/ProductionLine/ConveyorExecutor{T}.cs index a6e55296..4b937645 100644 --- a/src/Simplify.Pipelines/ProductionLine/ConveyorExecutor{T}.cs +++ b/src/Simplify.Pipelines/ProductionLine/ConveyorExecutor{T}.cs @@ -7,26 +7,20 @@ namespace Simplify.Pipelines.ProductionLine; /// /// /// -public class ConveyorExecutor : IConveyorExecutor +/// +/// Initializes a new instance of the class. +/// +/// The conveyor. +/// The conveyor item preparer. +/// +/// conveyor +/// or +/// itemPreparer +/// +public class ConveyorExecutor(IConveyor conveyor, IConveyorItemPreparer itemPreparer) : IConveyorExecutor { - private readonly IConveyor _conveyor; - private readonly IConveyorItemPreparer _itemPreparer; - - /// - /// Initializes a new instance of the class. - /// - /// The conveyor. - /// The conveyor item preparer. - /// - /// conveyor - /// or - /// itemPreparer - /// - public ConveyorExecutor(IConveyor conveyor, IConveyorItemPreparer itemPreparer) - { - _conveyor = conveyor ?? throw new ArgumentNullException(nameof(conveyor)); - _itemPreparer = itemPreparer ?? throw new ArgumentNullException(nameof(itemPreparer)); - } + private readonly IConveyor _conveyor = conveyor ?? throw new ArgumentNullException(nameof(conveyor)); + private readonly IConveyorItemPreparer _itemPreparer = itemPreparer ?? throw new ArgumentNullException(nameof(itemPreparer)); /// /// Runs the conveyor diff --git a/src/Simplify.Pipelines/ProductionLine/Conveyor{T}.cs b/src/Simplify.Pipelines/ProductionLine/Conveyor{T}.cs index d83cddd2..46ce3a1a 100644 --- a/src/Simplify.Pipelines/ProductionLine/Conveyor{T}.cs +++ b/src/Simplify.Pipelines/ProductionLine/Conveyor{T}.cs @@ -8,19 +8,14 @@ namespace Simplify.Pipelines.ProductionLine; /// /// Conveyor item type /// -public class Conveyor : IConveyor +/// +/// Initializes a new instance of the class. +/// +/// The stages. +/// stages +public class Conveyor(IList> stages) : IConveyor { - private readonly IList> _stages; - - /// - /// Initializes a new instance of the class. - /// - /// The stages. - /// stages - public Conveyor(IList> stages) - { - _stages = stages ?? throw new ArgumentNullException(nameof(stages)); - } + private readonly IList> _stages = stages ?? throw new ArgumentNullException(nameof(stages)); /// /// Occurs when conveyor is about to execute. diff --git a/src/Simplify.Pipelines/Simplify.Pipelines.csproj b/src/Simplify.Pipelines/Simplify.Pipelines.csproj index aedbb66b..831f614a 100644 --- a/src/Simplify.Pipelines/Simplify.Pipelines.csproj +++ b/src/Simplify.Pipelines/Simplify.Pipelines.csproj @@ -1,13 +1,13 @@ - net6.0;netstandard2.0 + net10.0;net6.0;netstandard2.0 latest true true snupkg true - 0.9.3 + 1.0.0 Pipelines processing, validation patterns Simplify diff --git a/src/Simplify.Pipelines/Validation/DataRule.cs b/src/Simplify.Pipelines/Validation/DataRule.cs deleted file mode 100644 index 80ff9e05..00000000 --- a/src/Simplify.Pipelines/Validation/DataRule.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Simplify.Pipelines.Validation; - -/// -/// Provides validation rule which can retrieve and hold data used by other rules -/// -/// The type of the data. -/// -/// The type of the result. -/// -/// -public abstract class DataRule : Rule, IDataRule -{ - /// - /// Gets the rule data. - /// - /// - /// The rule data. - /// - public abstract TData Data { get; } -} \ No newline at end of file diff --git a/src/Simplify.Pipelines/Validation/IDataRule{T}.cs b/src/Simplify.Pipelines/Validation/IDataRule{T}.cs deleted file mode 100644 index 025ab798..00000000 --- a/src/Simplify.Pipelines/Validation/IDataRule{T}.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Simplify.Pipelines.Validation; - -/// -/// Represent validation rule which can retrieve and hold data used by other rules -/// -/// The type of the data. -/// -/// The type of the result. -/// -public interface IDataRule : IRule -{ - /// - /// Gets the data. - /// - /// - /// The data. - /// - TData Data { get; } -} \ No newline at end of file diff --git a/src/Simplify.Pipelines/Validation/IRule{T}.cs b/src/Simplify.Pipelines/Validation/IRule{T}.cs deleted file mode 100644 index b57cbe23..00000000 --- a/src/Simplify.Pipelines/Validation/IRule{T}.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Simplify.Pipelines.Validation; - -/// -/// Represent validation rule -/// -/// -/// The type of the result. -public interface IRule -{ - /// - /// Gets the result representing invalid result of rule validation. - /// - /// - /// The invalid validation result. - /// - TResult InvalidValidationResult { get; } - - /// - /// Validates the specified item through this rule. - /// - /// The item. - /// - bool Check(T item); - - /// - /// Perform some action in case of invalid rule status. - /// - void GenerateInvalidAction(); -} \ No newline at end of file diff --git a/src/Simplify.Pipelines/Validation/IValidationPipelineProcessor{T}.cs b/src/Simplify.Pipelines/Validation/IValidationPipelineProcessor{T}.cs deleted file mode 100644 index cecd7f3d..00000000 --- a/src/Simplify.Pipelines/Validation/IValidationPipelineProcessor{T}.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; - -namespace Simplify.Pipelines.Validation; - -/// -/// Represent validation pipeline -/// -/// -/// The type of the result. -public interface IValidationPipelineProcessor -{ - /// - /// Executes the validation pipeline. - /// - /// - IDictionary> Check(); -} \ No newline at end of file diff --git a/src/Simplify.Pipelines/Validation/IValidationPipeline{T}.cs b/src/Simplify.Pipelines/Validation/IValidationPipeline{T}.cs deleted file mode 100644 index 534c82ff..00000000 --- a/src/Simplify.Pipelines/Validation/IValidationPipeline{T}.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; - -namespace Simplify.Pipelines.Validation; - -/// -/// Represent validation pipeline -/// -/// -/// The type of the result. -public interface IValidationPipeline -{ - /// - /// Validation the specified item through pipeline rules. - /// - /// The item. - /// - IList Check(T item); -} \ No newline at end of file diff --git a/src/Simplify.Pipelines/Validation/Rule.cs b/src/Simplify.Pipelines/Validation/Rule.cs deleted file mode 100644 index 67db86c4..00000000 --- a/src/Simplify.Pipelines/Validation/Rule.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace Simplify.Pipelines.Validation; - -/// -/// Provides base validation rule -/// -/// -/// The type of the result. -/// -public abstract class Rule : IRule -{ - /// - /// Gets the validation result in case of rule is invalid. - /// - /// - /// The invalid validation result. - /// - public abstract TResult InvalidValidationResult { get; } - - /// - /// Validates the specified item through this rule. - /// - /// The item. - /// - public abstract bool Check(T item); - - /// - /// Generates the action for invalid rule. - /// - public virtual void GenerateInvalidAction() - { - } -} \ No newline at end of file diff --git a/src/Simplify.Pipelines/Validation/ValidationPipelineProcessor{T}.cs b/src/Simplify.Pipelines/Validation/ValidationPipelineProcessor{T}.cs deleted file mode 100644 index e0c520f7..00000000 --- a/src/Simplify.Pipelines/Validation/ValidationPipelineProcessor{T}.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace Simplify.Pipelines.Validation; - -/// -/// Provides default validation pipeline processor -/// -/// -/// The type of the result. -/// -public class ValidationPipelineProcessor : IValidationPipelineProcessor -{ - private readonly IValidationPipeline _pipeline; - private readonly IDataPreparer _dataPreparer; - - /// - /// Initializes a new instance of the class. - /// - /// The pipeline. - /// The data preparer. - public ValidationPipelineProcessor(IValidationPipeline pipeline, IDataPreparer dataPreparer) - { - _pipeline = pipeline; - _dataPreparer = dataPreparer; - } - - /// - /// Executes the validation pipeline. - /// - /// - public virtual IDictionary> Check() - { - var data = _dataPreparer.GetData(); - - return data.ToDictionary(item => item, item => _pipeline.Check(item)); - } -} \ No newline at end of file diff --git a/src/Simplify.Pipelines/Validation/ValidationPipeline{T}.cs b/src/Simplify.Pipelines/Validation/ValidationPipeline{T}.cs deleted file mode 100644 index 4bd69655..00000000 --- a/src/Simplify.Pipelines/Validation/ValidationPipeline{T}.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace Simplify.Pipelines.Validation; - -/// -/// Provides default validation pipeline -/// -/// -/// The type of the result. -/// -public class ValidationPipeline : IValidationPipeline -{ - private readonly IList> _rules; - - /// - /// Initializes a new instance of the class. - /// - /// The rules. - public ValidationPipeline(IList> rules) => _rules = rules; - - /// - /// Validation the specified item through pipeline rules. - /// - /// The item. - /// - public virtual IList Check(T item) => _rules.Where(x => !x.Check(item)).Select(x => x.InvalidValidationResult).ToList(); -} \ No newline at end of file diff --git a/src/Simplify.Repository.EntityFramework/CHANGELOG.md b/src/Simplify.Repository.EntityFramework/CHANGELOG.md index 8275563f..b3a7afba 100644 --- a/src/Simplify.Repository.EntityFramework/CHANGELOG.md +++ b/src/Simplify.Repository.EntityFramework/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## [0.5.0] - 2026-06-25 + +### Added + +- .NET 10 support + +### Removed + +- .NET 9 support + +### Fixed + +- `GenericRepository.GetSingleByQueryAsync` used `FirstAsync` instead of single-element semantics; it now uses `SingleOrDefaultAsync` to match the synchronous `GetSingleByQuery` +- `TransactUnitOfWork` now clears the transaction reference on dispose + +### Dependencies + +- Microsoft.EntityFrameworkCore.Relational bump to 10.0.9 + ## [0.4.0] - 2025-10-10 ### Removed diff --git a/src/Simplify.Repository.EntityFramework/GenericRepository.cs b/src/Simplify.Repository.EntityFramework/GenericRepository.cs index e1b4cd25..7bc7da46 100644 --- a/src/Simplify.Repository.EntityFramework/GenericRepository.cs +++ b/src/Simplify.Repository.EntityFramework/GenericRepository.cs @@ -49,7 +49,7 @@ public class GenericRepository(DbContext session) : IGenericRepository /// /// The query. /// - public Task GetSingleByQueryAsync(Expression> query) => Session.Set().FirstAsync(query); + public Task GetSingleByQueryAsync(Expression> query) => Session.Set().SingleOrDefaultAsync(query)!; /// /// Gets the first object by query. diff --git a/src/Simplify.Repository.EntityFramework/Simplify.Repository.EntityFramework.csproj b/src/Simplify.Repository.EntityFramework/Simplify.Repository.EntityFramework.csproj index b8f879f5..b064e5b8 100644 --- a/src/Simplify.Repository.EntityFramework/Simplify.Repository.EntityFramework.csproj +++ b/src/Simplify.Repository.EntityFramework/Simplify.Repository.EntityFramework.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 latest enable true @@ -8,7 +8,7 @@ snupkg true - 0.4 + 0.5.0 Simplify.Repository EntityFramework implementation Simplify @@ -27,7 +27,7 @@ - + diff --git a/src/Simplify.Repository.EntityFramework/TransactUnitOfWork.cs b/src/Simplify.Repository.EntityFramework/TransactUnitOfWork.cs index ead852d1..78eeb577 100644 --- a/src/Simplify.Repository.EntityFramework/TransactUnitOfWork.cs +++ b/src/Simplify.Repository.EntityFramework/TransactUnitOfWork.cs @@ -97,7 +97,10 @@ protected override void Dispose(bool disposing) if (!disposing) return; + // Disposing an uncommitted transaction rolls it back, preventing a leaked open transaction + // when the unit of work is disposed without an explicit Commit/Rollback. _transaction?.Dispose(); + _transaction = null; base.Dispose(disposing); } diff --git a/src/Simplify.Repository.FluentNHibernate.Tests/Simplify.Repository.FluentNHibernate.Tests.csproj b/src/Simplify.Repository.FluentNHibernate.Tests/Simplify.Repository.FluentNHibernate.Tests.csproj index c95619d1..8cd4e32c 100644 --- a/src/Simplify.Repository.FluentNHibernate.Tests/Simplify.Repository.FluentNHibernate.Tests.csproj +++ b/src/Simplify.Repository.FluentNHibernate.Tests/Simplify.Repository.FluentNHibernate.Tests.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 latest Simplify.FluentNHibernate.Repository unit tests diff --git a/src/Simplify.Repository/CHANGELOG.md b/src/Simplify.Repository/CHANGELOG.md index fc352d41..5c1434db 100644 --- a/src/Simplify.Repository/CHANGELOG.md +++ b/src/Simplify.Repository/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [1.7.1] - 2026-06-25 + +### Added + +- .NET 10 support + +### Fixed + +- `CommonEqualityComparer.GetHashCode` was inconsistent with `Equals` (it returned the reference-based hash code while `Equals` compared by `ID`), so equal entities ended up in different buckets in hash-based collections (`Dictionary`, `HashSet`, `Distinct`, `GroupBy`); the hash code is now derived from `ID`, and `null` is handled +- `TransactGenericRepository` did not roll back the transaction when a repository operation threw, leaving an open transaction on the connection; all operations now roll back on failure + ## [1.7.0] - 2024-01-14 ### Changed diff --git a/src/Simplify.Repository/CommonEqualityComparer.cs b/src/Simplify.Repository/CommonEqualityComparer.cs index ab90d2a8..e2512a5e 100644 --- a/src/Simplify.Repository/CommonEqualityComparer.cs +++ b/src/Simplify.Repository/CommonEqualityComparer.cs @@ -34,5 +34,14 @@ public class CommonEqualityComparer : IEqualityComparer /// /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. /// - public int GetHashCode(object obj) => obj.GetHashCode(); + public int GetHashCode(object obj) + { + if (obj == null) + return 0; + + if (obj is IIdentityObject identityObject) + return identityObject.ID.GetHashCode(); + + return obj.GetHashCode(); + } } \ No newline at end of file diff --git a/src/Simplify.Repository/Simplify.Repository.csproj b/src/Simplify.Repository/Simplify.Repository.csproj index 858c24af..46754989 100644 --- a/src/Simplify.Repository/Simplify.Repository.csproj +++ b/src/Simplify.Repository/Simplify.Repository.csproj @@ -1,6 +1,6 @@ - net6.0;netstandard2.1;netstandard2.0;net48 + net10.0;net6.0;netstandard2.1;netstandard2.0;net48 latest enable true @@ -8,7 +8,7 @@ snupkg true - 1.7 + 1.7.1 Repository, unit of work and related patterns interfaces Simplify diff --git a/src/Simplify.Repository/TransactGenericRepository`1.cs b/src/Simplify.Repository/TransactGenericRepository`1.cs index dfd5e1ac..952626ce 100644 --- a/src/Simplify.Repository/TransactGenericRepository`1.cs +++ b/src/Simplify.Repository/TransactGenericRepository`1.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data; using System.Linq; @@ -12,26 +12,15 @@ namespace Simplify.Repository; /// /// /// -public class TransactGenericRepository : IGenericRepository +/// +/// Initializes a new instance of the class. +/// +/// The base repository. +/// The unit of work. +/// The isolation level. +public class TransactGenericRepository(IGenericRepository baseRepository, ITransactUnitOfWork unitOfWork, IsolationLevel isolationLevel = IsolationLevel.ReadCommitted) : IGenericRepository where T : class { - private readonly IGenericRepository _baseRepository; - private readonly IsolationLevel _isolationLevel; - private readonly ITransactUnitOfWork _unitOfWork; - - /// - /// Initializes a new instance of the class. - /// - /// The base repository. - /// The unit of work. - /// The isolation level. - public TransactGenericRepository(IGenericRepository baseRepository, ITransactUnitOfWork unitOfWork, IsolationLevel isolationLevel = IsolationLevel.ReadCommitted) - { - _baseRepository = baseRepository; - _unitOfWork = unitOfWork; - _isolationLevel = isolationLevel; - } - /// /// Adds the object. /// @@ -39,22 +28,7 @@ public TransactGenericRepository(IGenericRepository baseRepository, ITransact /// /// The generated identifier /// - public object Add(T entity) - { - var skip = false; - - if (_unitOfWork.IsTransactionActive) - skip = true; - else - _unitOfWork.BeginTransaction(_isolationLevel); - - var result = _baseRepository.Add(entity); - - if (!skip) - _unitOfWork.Commit(); - - return result; - } + public object Add(T entity) => Execute(() => baseRepository.Add(entity)); /// /// Adds the object asynchronously. @@ -63,192 +37,61 @@ public object Add(T entity) /// /// The generated identifier /// - public async Task AddAsync(T entity) - { - var skip = false; - - if (_unitOfWork.IsTransactionActive) - skip = true; - else - _unitOfWork.BeginTransaction(_isolationLevel); - - var result = await _baseRepository.AddAsync(entity); - - if (!skip) - await _unitOfWork.CommitAsync(); - - return result; - } + public Task AddAsync(T entity) => ExecuteAsync(() => baseRepository.AddAsync(entity)); /// /// Deletes the object. /// /// The entity. - public void Delete(T entity) - { - var skip = false; - - if (_unitOfWork.IsTransactionActive) - skip = true; - else - _unitOfWork.BeginTransaction(_isolationLevel); - - _baseRepository.Delete(entity); - - if (!skip) - _unitOfWork.Commit(); - } + public void Delete(T entity) => Execute(() => baseRepository.Delete(entity)); /// /// Deletes the object asynchronously. /// /// The entity. - public async Task DeleteAsync(T entity) - { - var skip = false; - - if (_unitOfWork.IsTransactionActive) - skip = true; - else - _unitOfWork.BeginTransaction(_isolationLevel); - - await _baseRepository.DeleteAsync(entity); - - if (!skip) - await _unitOfWork.CommitAsync(); - } + public Task DeleteAsync(T entity) => ExecuteAsync(() => baseRepository.DeleteAsync(entity)); /// /// Gets the number of elements. /// /// The query. /// - public int GetCount(Expression>? query = null) - { - var skip = false; - - if (_unitOfWork.IsTransactionActive) - skip = true; - else - _unitOfWork.BeginTransaction(_isolationLevel); - - var result = _baseRepository.GetCount(query); - - if (!skip) - _unitOfWork.Commit(); - - return result; - } + public int GetCount(Expression>? query = null) => Execute(() => baseRepository.GetCount(query)); /// /// Gets the number of elements asynchronously. /// /// The query. /// - public async Task GetCountAsync(Expression>? query = null) - { - var skip = false; - - if (_unitOfWork.IsTransactionActive) - skip = true; - else - _unitOfWork.BeginTransaction(_isolationLevel); - - var result = await _baseRepository.GetCountAsync(query); - - if (!skip) - await _unitOfWork.CommitAsync(); - - return result; - } + public Task GetCountAsync(Expression>? query = null) => ExecuteAsync(() => baseRepository.GetCountAsync(query)); /// /// Gets the first object by query. /// /// The query. /// - public T GetFirstByQuery(Expression> query) - { - var skip = false; - - if (_unitOfWork.IsTransactionActive) - skip = true; - else - _unitOfWork.BeginTransaction(_isolationLevel); - - var result = _baseRepository.GetFirstByQuery(query); - - if (!skip) - _unitOfWork.Commit(); - - return result; - } + public T GetFirstByQuery(Expression> query) => Execute(() => baseRepository.GetFirstByQuery(query)); /// /// Gets the first object by query asynchronously. /// /// The query. /// - public async Task GetFirstByQueryAsync(Expression> query) - { - var skip = false; - - if (_unitOfWork.IsTransactionActive) - skip = true; - else - _unitOfWork.BeginTransaction(_isolationLevel); - - var result = await _baseRepository.GetFirstByQueryAsync(query); - - if (!skip) - await _unitOfWork.CommitAsync(); - - return result; - } + public Task GetFirstByQueryAsync(Expression> query) => ExecuteAsync(() => baseRepository.GetFirstByQueryAsync(query)); /// /// Gets the long number of elements. /// /// The query. /// - public long GetLongCount(Expression>? query = null) - { - var skip = false; - - if (_unitOfWork.IsTransactionActive) - skip = true; - else - _unitOfWork.BeginTransaction(_isolationLevel); - - var result = _baseRepository.GetLongCount(query); - - if (!skip) - _unitOfWork.Commit(); - - return result; - } + public long GetLongCount(Expression>? query = null) => Execute(() => baseRepository.GetLongCount(query)); /// /// Gets the long number of elements asynchronously. /// /// The query. /// - public async Task GetLongCountAsync(Expression>? query = null) - { - var skip = false; - - if (_unitOfWork.IsTransactionActive) - skip = true; - else - _unitOfWork.BeginTransaction(_isolationLevel); - - var result = await _baseRepository.GetLongCountAsync(query); - - if (!skip) - await _unitOfWork.CommitAsync(); - - return result; - } + public Task GetLongCountAsync(Expression>? query = null) => ExecuteAsync(() => baseRepository.GetLongCountAsync(query)); /// /// Gets the multiple objects by query or all objects without query. @@ -257,22 +100,8 @@ public async Task GetLongCountAsync(Expression>? query = nul /// The custom processing. /// public IList GetMultiple(Expression>? query = null, - Func, IQueryable>? customProcessing = null) - { - var skip = false; - - if (_unitOfWork.IsTransactionActive) - skip = true; - else - _unitOfWork.BeginTransaction(_isolationLevel); - - var result = _baseRepository.GetMultiple(query, customProcessing); - - if (!skip) - _unitOfWork.Commit(); - - return result; - } + Func, IQueryable>? customProcessing = null) => + Execute(() => baseRepository.GetMultiple(query, customProcessing)); /// /// Gets the multiple objects by query or all objects without query asynchronously. @@ -280,23 +109,9 @@ public IList GetMultiple(Expression>? query = null, /// The query. /// The custom processing. /// - public async Task> GetMultipleAsync(Expression>? query = null, - Func, IQueryable>? customProcessing = null) - { - var skip = false; - - if (_unitOfWork.IsTransactionActive) - skip = true; - else - _unitOfWork.BeginTransaction(_isolationLevel); - - var result = await _baseRepository.GetMultipleAsync(query, customProcessing); - - if (!skip) - await _unitOfWork.CommitAsync(); - - return result; - } + public Task> GetMultipleAsync(Expression>? query = null, + Func, IQueryable>? customProcessing = null) => + ExecuteAsync(() => baseRepository.GetMultipleAsync(query, customProcessing)); /// /// Gets the multiple paged elements list. @@ -307,22 +122,8 @@ public async Task> GetMultipleAsync(Expression>? query = /// The custom processing. /// public IList GetPaged(int pageIndex, int itemsPerPage, Expression>? query = null, - Func, IQueryable>? customProcessing = null) - { - var skip = false; - - if (_unitOfWork.IsTransactionActive) - skip = true; - else - _unitOfWork.BeginTransaction(_isolationLevel); - - var result = _baseRepository.GetPaged(pageIndex, itemsPerPage, query, customProcessing); - - if (!skip) - _unitOfWork.Commit(); - - return result; - } + Func, IQueryable>? customProcessing = null) => + Execute(() => baseRepository.GetPaged(pageIndex, itemsPerPage, query, customProcessing)); /// /// Gets the multiple paged elements list asynchronously. @@ -332,147 +133,109 @@ public IList GetPaged(int pageIndex, int itemsPerPage, ExpressionThe query. /// The custom processing. /// - public async Task> GetPagedAsync(int pageIndex, int itemsPerPage, Expression>? query = null, - Func, IQueryable>? customProcessing = null) - { - var skip = false; - - if (_unitOfWork.IsTransactionActive) - skip = true; - else - _unitOfWork.BeginTransaction(_isolationLevel); - - var result = await _baseRepository.GetPagedAsync(pageIndex, itemsPerPage, query, customProcessing); - - if (!skip) - await _unitOfWork.CommitAsync(); - - return result; - } + public Task> GetPagedAsync(int pageIndex, int itemsPerPage, Expression>? query = null, + Func, IQueryable>? customProcessing = null) => + ExecuteAsync(() => baseRepository.GetPagedAsync(pageIndex, itemsPerPage, query, customProcessing)); /// /// Gets the single object by identifier. /// /// The identifier. /// - public T GetSingleByID(object id) - { - var skip = false; - - if (_unitOfWork.IsTransactionActive) - skip = true; - else - _unitOfWork.BeginTransaction(_isolationLevel); - - var result = _baseRepository.GetSingleByID(id); - - if (!skip) - _unitOfWork.Commit(); - - return result; - } + public T GetSingleByID(object id) => Execute(() => baseRepository.GetSingleByID(id)); /// /// Gets the single object by identifier asynchronously. /// /// The identifier. /// - public async Task GetSingleByIDAsync(object id) - { - var skip = false; - - if (_unitOfWork.IsTransactionActive) - skip = true; - else - _unitOfWork.BeginTransaction(_isolationLevel); - - var result = await _baseRepository.GetSingleByIDAsync(id); - - if (!skip) - await _unitOfWork.CommitAsync(); - - return result; - } + public Task GetSingleByIDAsync(object id) => ExecuteAsync(() => baseRepository.GetSingleByIDAsync(id)); /// /// Gets the single object by query. /// /// The query. /// - public T GetSingleByQuery(Expression> query) - { - var skip = false; - - if (_unitOfWork.IsTransactionActive) - skip = true; - else - _unitOfWork.BeginTransaction(_isolationLevel); - - var result = _baseRepository.GetSingleByQuery(query); - - if (!skip) - _unitOfWork.Commit(); - - return result; - } + public T GetSingleByQuery(Expression> query) => Execute(() => baseRepository.GetSingleByQuery(query)); /// /// Gets the single object by query asynchronously. /// /// The query. /// - public async Task GetSingleByQueryAsync(Expression> query) - { - var skip = false; - - if (_unitOfWork.IsTransactionActive) - skip = true; - else - _unitOfWork.BeginTransaction(_isolationLevel); - - var result = await _baseRepository.GetSingleByQueryAsync(query); - - if (!skip) - await _unitOfWork.CommitAsync(); - - return result; - } + public Task GetSingleByQueryAsync(Expression> query) => ExecuteAsync(() => baseRepository.GetSingleByQueryAsync(query)); /// /// Updates the object. /// /// The entity. - public void Update(T entity) + public void Update(T entity) => Execute(() => baseRepository.Update(entity)); + + /// + /// Updates the object asynchronously. + /// + /// The entity. + public Task UpdateAsync(T entity) => ExecuteAsync(() => baseRepository.UpdateAsync(entity)); + + private TResult Execute(Func operation) { - var skip = false; + if (unitOfWork.IsTransactionActive) + return operation(); - if (_unitOfWork.IsTransactionActive) - skip = true; - else - _unitOfWork.BeginTransaction(_isolationLevel); + unitOfWork.BeginTransaction(isolationLevel); - _baseRepository.Update(entity); + try + { + var result = operation(); - if (!skip) - _unitOfWork.Commit(); + unitOfWork.Commit(); + + return result; + } + catch + { + unitOfWork.Rollback(); + + throw; + } } - /// - /// Updates the object asynchronously. - /// - /// The entity. - public async Task UpdateAsync(T entity) + private void Execute(Action operation) => + Execute(() => + { + operation(); + + return true; + }); + + private async Task ExecuteAsync(Func> operation) { - var skip = false; + if (unitOfWork.IsTransactionActive) + return await operation(); - if (_unitOfWork.IsTransactionActive) - skip = true; - else - _unitOfWork.BeginTransaction(_isolationLevel); + unitOfWork.BeginTransaction(isolationLevel); - await _baseRepository.UpdateAsync(entity); + try + { + var result = await operation(); - if (!skip) - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); + + return result; + } + catch + { + await unitOfWork.RollbackAsync(); + + throw; + } } -} \ No newline at end of file + + private async Task ExecuteAsync(Func operation) => + await ExecuteAsync(async () => + { + await operation(); + + return true; + }); +} diff --git a/src/Simplify.Resources.Tests/ResourcesStringTableTests.cs b/src/Simplify.Resources.Tests/ResourcesStringTableTests.cs index bd926b41..0f626fec 100644 --- a/src/Simplify.Resources.Tests/ResourcesStringTableTests.cs +++ b/src/Simplify.Resources.Tests/ResourcesStringTableTests.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using System.Collections.Generic; +using System.Reflection; using NUnit.Framework; namespace Simplify.Resources.Tests; @@ -50,4 +51,40 @@ public void ResourcesStringTableIndexer_NoExistingString_Null() // Assert Assert.That(testString, Is.Null); } + + [Test] + public void ResourcesStringTableGetString_NoExistingString_Null() + { + // Arrange + _uow = new ResourcesStringTable(true, "ProgramResources"); + + // Act + var result = _uow.GetString("NonExistentKey"); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public void ResourcesStringTableGetRequiredString_ExistingString_ReturnsValue() + { + // Arrange + _uow = new ResourcesStringTable(true, "ProgramResources"); + + // Act + var result = _uow.GetRequiredString("TestString"); + + // Assert + Assert.That(result, Is.EqualTo("Hello World!")); + } + + [Test] + public void ResourcesStringTableGetRequiredString_NoExistingString_ThrowsKeyNotFoundException() + { + // Arrange + _uow = new ResourcesStringTable(true, "ProgramResources"); + + // Act & Assert + Assert.Throws(() => _uow.GetRequiredString("NonExistentKey")); + } } \ No newline at end of file diff --git a/src/Simplify.Resources.Tests/Simplify.Resources.Tests.csproj b/src/Simplify.Resources.Tests/Simplify.Resources.Tests.csproj index c5fcd303..aca3dfb4 100644 --- a/src/Simplify.Resources.Tests/Simplify.Resources.Tests.csproj +++ b/src/Simplify.Resources.Tests/Simplify.Resources.Tests.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 Simplify.System unit tests Simplify diff --git a/src/Simplify.Resources/CHANGELOG.md b/src/Simplify.Resources/CHANGELOG.md index bb8a56fd..d620a756 100644 --- a/src/Simplify.Resources/CHANGELOG.md +++ b/src/Simplify.Resources/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [1.1.0] - 2026-06-25 + +### Added + +- .NET 10 support +- `ResourcesStringTable.GetRequiredString`, throws `KeyNotFoundException` if key is not found + +### Fixed + +- `ResourcesStringTable` now throws a descriptive `InvalidOperationException` instead of a `NullReferenceException` when the entry assembly cannot be resolved (e.g. in unmanaged/host scenarios) +- `ResourcesStringTable(Assembly, ...)` constructor now validates the assembly parameter with `ArgumentNullException` +- `StringTable.Entry` setter now uses `nameof(value)` in `ArgumentNullException` instead of an empty parameter name +- `StringTable.Entry` lazy initialization was not thread-safe (`??=`) and could allocate duplicate instances under concurrent first access; initialization is now synchronized with double-checked locking + ## [1.0.3] - 2023-08-01 ### Removed diff --git a/src/Simplify.Resources/IResourcesStringTable.cs b/src/Simplify.Resources/IResourcesStringTable.cs index 1d4dfcc7..b5470117 100644 --- a/src/Simplify.Resources/IResourcesStringTable.cs +++ b/src/Simplify.Resources/IResourcesStringTable.cs @@ -11,7 +11,12 @@ public interface IResourcesStringTable string this[string name] { get; } /// - /// Get string table record by name + /// Get string table record by name (returns null if key not found) /// string GetString(string name); + + /// + /// Get string table record by name, throws KeyNotFoundException if key not found + /// + string GetRequiredString(string name); } \ No newline at end of file diff --git a/src/Simplify.Resources/ResourcesStringTable.cs b/src/Simplify.Resources/ResourcesStringTable.cs index 03a848af..d62bb1cd 100644 --- a/src/Simplify.Resources/ResourcesStringTable.cs +++ b/src/Simplify.Resources/ResourcesStringTable.cs @@ -1,4 +1,6 @@ -using System.Reflection; +using System; +using System.Collections.Generic; +using System.Reflection; using System.Resources; namespace Simplify.Resources; @@ -32,7 +34,7 @@ public ResourcesStringTable(bool callingAssembly, string resourcesFileName = "Re /// The root name of the resources (Assembly name will be used by default). public ResourcesStringTable(Assembly assembly, string resourcesFileName = "Resources", string baseName = null) { - _workingAssembly = assembly; + _workingAssembly = assembly ?? throw new ArgumentNullException(nameof(assembly)); InitializeResourceManager(resourcesFileName, baseName); } @@ -42,12 +44,20 @@ public ResourcesStringTable(Assembly assembly, string resourcesFileName = "Resou public string this[string name] => GetString(name); /// - /// Get string table record by name + /// Get string table record by name (returns null if key not found) /// public string GetString(string name) => _resourceManager.GetString(name); + /// + /// Get string table record by name, throws KeyNotFoundException if key not found + /// + public string GetRequiredString(string name) => _resourceManager.GetString(name) ?? throw new KeyNotFoundException($"Resource key '{name}' not found in string table."); + private void InitializeResourceManager(string resourcesFileName = "Resources", string baseName = null) { + if (_workingAssembly == null) + throw new InvalidOperationException("Unable to resolve the working assembly for the resources string table (entry assembly is null in the current host). Use the constructor accepting an explicit assembly."); + if (baseName == null) baseName = _workingAssembly.GetName().Name; diff --git a/src/Simplify.Resources/Simplify.Resources.csproj b/src/Simplify.Resources/Simplify.Resources.csproj index 05a728ab..cda493fd 100644 --- a/src/Simplify.Resources/Simplify.Resources.csproj +++ b/src/Simplify.Resources/Simplify.Resources.csproj @@ -1,6 +1,6 @@ - net6.0;netstandard2.1;netstandard2.0;net48 + net10.0;net6.0;netstandard2.1;netstandard2.0;net48 latest false @@ -9,7 +9,7 @@ snupkg true - 1.0.3 + 1.1.0 Classes for working with assembly resource files (localizable string table) Simplify diff --git a/src/Simplify.Resources/StringTable.cs b/src/Simplify.Resources/StringTable.cs index 7a5d3b3f..ba2dc89e 100644 --- a/src/Simplify.Resources/StringTable.cs +++ b/src/Simplify.Resources/StringTable.cs @@ -9,12 +9,21 @@ public static class StringTable { private static IResourcesStringTable _entryStringTable; + private static readonly object EntryLocker = new(); + /// /// Entry assembly string table (ProgramResources.resx) /// public static IResourcesStringTable Entry { - get => _entryStringTable ??= new ResourcesStringTable(false, "ProgramResources"); - set => _entryStringTable = value ?? throw new ArgumentNullException(); + get + { + if (_entryStringTable != null) + return _entryStringTable; + + lock (EntryLocker) + return _entryStringTable ??= new ResourcesStringTable(false, "ProgramResources"); + } + set => _entryStringTable = value ?? throw new ArgumentNullException(nameof(value)); } } \ No newline at end of file diff --git a/src/Simplify.Scheduler.IntegrationTester/Program.cs b/src/Simplify.Scheduler.IntegrationTester/Program.cs index fcad649d..b32c8d16 100644 --- a/src/Simplify.Scheduler.IntegrationTester/Program.cs +++ b/src/Simplify.Scheduler.IntegrationTester/Program.cs @@ -26,7 +26,6 @@ if (await scheduler.StartAsync(args)) return; - // Testing some processors without scheduler using var scope = DIContainer.Current.BeginLifetimeScope(); diff --git a/src/Simplify.Scheduler.IntegrationTester/Simplify.Scheduler.IntegrationTester.csproj b/src/Simplify.Scheduler.IntegrationTester/Simplify.Scheduler.IntegrationTester.csproj index a30bfdda..3a5fb9c2 100644 --- a/src/Simplify.Scheduler.IntegrationTester/Simplify.Scheduler.IntegrationTester.csproj +++ b/src/Simplify.Scheduler.IntegrationTester/Simplify.Scheduler.IntegrationTester.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 Exe false diff --git a/src/Simplify.Scheduler/CHANGELOG.md b/src/Simplify.Scheduler/CHANGELOG.md index a1dee431..8320516d 100644 --- a/src/Simplify.Scheduler/CHANGELOG.md +++ b/src/Simplify.Scheduler/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## [1.7.0] - 2026-06-25 + +### Fixed + +- Crontab job timers were never stopped/disposed on shutdown, leaking timers that kept firing during and after stop; all job timers are now stopped before waiting for running tasks and on dispose +- Data race in `CrontabProcessor` between `IsMatching` and `CalculateNextOccurrences` over the shared occurrences list; access is now synchronized +- Lifetime scope leak in basic job execution when resolving or invoking the job throws; the scope is now disposed on failure +- `MultitaskScheduler` now unsubscribes from the static `Console.CancelKeyPress` event on dispose, preventing the disposed instance from being rooted and from receiving Ctrl+C after disposal +- Exceptions thrown from the crontab timer callback (`OnCronTimerTick`/`OnStartWork`) escaped to a thread-pool thread and terminated the process; they are now routed to the `OnException` event +- Unhandled job exceptions were rethrown from inside the worker task, surfacing as an `AggregateException` from `Task.WaitAll` and aborting graceful shutdown; they are no longer rethrown +- Basic (long-running) job lifetime scope was registered for disposal only after the job method returned, leaking the scope for never-completing jobs and blocking startup; the scope is now registered before the job runs and the job is executed without blocking startup + +### Added + +- .NET 10 support + +### Dependencies + +- Microsoft.Extensions.Configuration bump to 10.0.9 + ## [1.6.1] - 2025-10-10 ### Dependencies diff --git a/src/Simplify.Scheduler/Jobs/Crontab/CrontabProcessor.cs b/src/Simplify.Scheduler/Jobs/Crontab/CrontabProcessor.cs index d93739ef..4de11419 100644 --- a/src/Simplify.Scheduler/Jobs/Crontab/CrontabProcessor.cs +++ b/src/Simplify.Scheduler/Jobs/Crontab/CrontabProcessor.cs @@ -12,6 +12,8 @@ namespace Simplify.Scheduler.Jobs.Crontab; /// public class CrontabProcessor : ICrontabProcessor { + private readonly object _occurrencesLocker = new object(); + /// /// Initializes a new instance of the class. /// @@ -67,10 +69,13 @@ public CrontabProcessor(string crontabExpression) /// The base time. public void CalculateNextOccurrences(DateTime baseTime) { - NextOccurrences.Clear(); + lock (_occurrencesLocker) + { + NextOccurrences.Clear(); - foreach (var schedule in Schedules) - NextOccurrences.Add(schedule.GetNextOccurrence(baseTime)); + foreach (var schedule in Schedules) + NextOccurrences.Add(schedule.GetNextOccurrence(baseTime)); + } } /// @@ -80,11 +85,12 @@ public void CalculateNextOccurrences(DateTime baseTime) /// public bool IsMatching(DateTime time) { - return - NextOccurrences.Any( - occurrence => - time.Year == occurrence.Year && time.Month == occurrence.Month && time.Day == occurrence.Day && - time.Hour == occurrence.Hour && time.Minute == occurrence.Minute); + lock (_occurrencesLocker) + return + NextOccurrences.Any( + occurrence => + time.Year == occurrence.Year && time.Month == occurrence.Month && time.Day == occurrence.Day && + time.Hour == occurrence.Hour && time.Minute == occurrence.Minute); } /// diff --git a/src/Simplify.Scheduler/MultitaskScheduler.cs b/src/Simplify.Scheduler/MultitaskScheduler.cs index 4618ed60..a2b39c95 100644 --- a/src/Simplify.Scheduler/MultitaskScheduler.cs +++ b/src/Simplify.Scheduler/MultitaskScheduler.cs @@ -42,7 +42,13 @@ public ICommandLineProcessor CommandLineProcessor /// Starts the scheduler asynchronously. /// /// The arguments. - public async Task StartAsync(string[]? args = null) + public Task StartAsync(string[]? args = null) => Task.FromResult(Start(args)); + + /// + /// Starts the scheduler. + /// + /// The arguments. + public bool Start(string[]? args = null) { var commandLineProcessResult = CommandLineProcessor.ProcessCommandLineArguments(args); @@ -52,7 +58,7 @@ public async Task StartAsync(string[]? args = null) return false; case ProcessCommandLineResult.NoArguments: - await StartAsync(); + StartScheduler(); break; case ProcessCommandLineResult.UndefinedParameters: @@ -68,16 +74,6 @@ public async Task StartAsync(string[]? args = null) return true; } - /// - /// Starts the scheduler. - /// - /// The arguments. - public bool Start(string[]? args = null) => - StartAsync(args) - .ConfigureAwait(false) - .GetAwaiter() - .GetResult(); - /// /// Called when scheduler is about to stop, main stopping point /// @@ -96,14 +92,17 @@ protected void StopJobs(object? sender, ConsoleCancelEventArgs args) // ReSharper disable once FlagArgument protected override void Dispose(bool disposing) { + if (disposing) + Console.CancelKeyPress -= StopJobs; + base.Dispose(disposing); _closing.Dispose(); } - private async Task StartAsync() + private void StartScheduler() { - await StartJobsAsync(); + StartJobs(); Console.WriteLine("Scheduler started. Press Ctrl + C to shut down."); diff --git a/src/Simplify.Scheduler/SchedulerJobsHandler.cs b/src/Simplify.Scheduler/SchedulerJobsHandler.cs index 11bf2eae..e933e8cf 100644 --- a/src/Simplify.Scheduler/SchedulerJobsHandler.cs +++ b/src/Simplify.Scheduler/SchedulerJobsHandler.cs @@ -141,7 +141,7 @@ public void Dispose() /// /// Called when scheduler is started, main execution starting point. /// - protected async Task StartJobsAsync() + protected void StartJobs() { Console.WriteLine("Starting Scheduler jobs..."); @@ -151,8 +151,8 @@ protected async Task StartJobsAsync() { job.Start(); - if (!(job is ICrontabSchedulerJob)) - await RunBasicJobAsync(job); + if (job is not ICrontabSchedulerJob) + RunBasicJob(job); } } catch (Exception e) @@ -173,10 +173,15 @@ protected void StopJobs() Console.WriteLine("Scheduler stopping, waiting for jobs to finish..."); ShutdownInProcess = true; + + // Stop all job timers first so no new tasks are spawned while we wait for the running ones to finish. + foreach (var job in _jobs) + job.Stop(); + Task[] itemsToWait; lock (_workingJobsTasks) - itemsToWait = _workingJobsTasks.Select(x => x.Task).ToArray(); + itemsToWait = [.. _workingJobsTasks.Select(x => x.Task)]; Task.WaitAll(itemsToWait); @@ -209,11 +214,20 @@ protected virtual void Dispose(bool disposing) if (!disposing) return; - foreach (var basicJobItem in _workingBasicJobs) + // Stop and dispose all job timers (idempotent if already stopped in StopJobs) to avoid leaking them. + foreach (var job in _jobs) + job.Stop(); + + lock (_workingBasicJobs) { - OnJobFinish?.Invoke(basicJobItem.Key); + foreach (var basicJobItem in _workingBasicJobs) + { + OnJobFinish?.Invoke(basicJobItem.Key); - basicJobItem.Value.Dispose(); + basicJobItem.Value.Dispose(); + } + + _workingBasicJobs.Clear(); } } @@ -227,38 +241,54 @@ private void InitializeJob(ICrontabSchedulerJob job) private void OnCronTimerTick(object? state) { - if (state is null) - throw new ArgumentNullException(nameof(state)); + try + { + if (state is null) + throw new ArgumentNullException(nameof(state)); - var job = (ICrontabSchedulerJob)state; + var job = (ICrontabSchedulerJob)state; - if (job.CrontabProcessor == null) - throw new InvalidOperationException($"{nameof(job.CrontabProcessor)} is null"); + if (job.CrontabProcessor == null) + throw new InvalidOperationException($"{nameof(job.CrontabProcessor)} is null"); - if (!job.CrontabProcessor.IsMatching()) - return; + if (!job.CrontabProcessor.IsMatching()) + return; - job.CrontabProcessor.CalculateNextOccurrences(); + job.CrontabProcessor.CalculateNextOccurrences(); - OnStartWork(state); + OnStartWork(state); + } + catch (Exception e) + { + if (!TryRaiseOnExceptionEvent(e)) + Console.Error.WriteLine($"Unhandled exception in scheduler timer callback: {e}"); + } } private void OnStartWork(object? state) { - if (state is null) - throw new ArgumentNullException(nameof(state)); + try + { + if (state is null) + throw new ArgumentNullException(nameof(state)); - var job = (ICrontabSchedulerJob)state; + var job = (ICrontabSchedulerJob)state; - lock (_workingJobsTasks) - { - if (ShutdownInProcess || _workingJobsTasks.Count(x => x.Job == job) >= job.Settings.MaximumParallelTasksCount) - return; + lock (_workingJobsTasks) + { + if (ShutdownInProcess || _workingJobsTasks.Count(x => x.Job == job) >= job.Settings.MaximumParallelTasksCount) + return; - _jobTaskID++; + _jobTaskID++; - _workingJobsTasks.Add(new CrontabSchedulerJobTask(_jobTaskID, job, - Task.Factory.StartNew(Run, new Tuple(_jobTaskID, job)).Unwrap())); + _workingJobsTasks.Add(new CrontabSchedulerJobTask(_jobTaskID, job, + Task.Factory.StartNew(Run, new Tuple(_jobTaskID, job)).Unwrap())); + } + } + catch (Exception e) + { + if (!TryRaiseOnExceptionEvent(e)) + Console.Error.WriteLine($"Unhandled exception in scheduler timer callback: {e}"); } } @@ -278,7 +308,7 @@ private async Task Run(object? state) catch (Exception e) { if (!TryRaiseOnExceptionEvent(e)) - throw; + Console.Error.WriteLine($"Unhandled exception in scheduler job '{job.JobClassType.Name}': {e}"); } finally { @@ -299,24 +329,52 @@ private async Task RunScopedAsync(ISchedulerJobRepresentation job) OnJobFinish?.Invoke(job); } - private async Task RunBasicJobAsync(ISchedulerJobRepresentation job) + private void RunBasicJob(ISchedulerJobRepresentation job) { + var scope = DIContainer.Current.BeginLifetimeScope(); + + object jobObject; + try { - var scope = DIContainer.Current.BeginLifetimeScope(); - - var jobObject = scope.Resolver.Resolve(job.JobClassType); + jobObject = scope.Resolver.Resolve(job.JobClassType); OnJobStart?.Invoke(job); - - await InvokeJobMethodAsync(job, jobObject); - - _workingBasicJobs.Add(job, scope); } catch (Exception e) { + scope.Dispose(); + if (!TryRaiseOnExceptionEvent(e)) throw; + + return; + } + + // The scope must be registered for disposal before awaiting: basic jobs are typically long-running + // (server-style) and the invocation may never complete, so registering it after the await would leak the scope. + lock (_workingBasicJobs) + _workingBasicJobs.Add(job, scope); + + // Basic jobs must not block startup, so the (possibly non-completing) job method is run as a background task. + _ = InvokeJobMethodAsync(job, jobObject).ContinueWith(t => + { + RemoveAndDisposeBasicJob(job); + + if (!TryRaiseOnExceptionEvent(t.Exception!.GetBaseException())) + Console.Error.WriteLine($"Unhandled exception in basic scheduler job '{job.JobClassType.Name}': {t.Exception!.GetBaseException()}"); + }, TaskContinuationOptions.OnlyOnFaulted); + } + + private void RemoveAndDisposeBasicJob(ISchedulerJobRepresentation job) + { + lock (_workingBasicJobs) + { + if (!_workingBasicJobs.TryGetValue(job, out var scope)) + return; + + scope.Dispose(); + _workingBasicJobs.Remove(job); } } diff --git a/src/Simplify.Scheduler/Simplify.Scheduler.csproj b/src/Simplify.Scheduler/Simplify.Scheduler.csproj index 37ba3697..db51aca5 100644 --- a/src/Simplify.Scheduler/Simplify.Scheduler.csproj +++ b/src/Simplify.Scheduler/Simplify.Scheduler.csproj @@ -1,6 +1,6 @@ - net9.0;net8.0;netstandard2.1;netstandard2.0;net48 + net10.0;net9.0;net8.0;netstandard2.1;netstandard2.0;net48 latest enable true @@ -8,7 +8,7 @@ snupkg true - 1.6.1 + 1.7.0 Scheduler framework with DI Simplify @@ -27,7 +27,7 @@ - + diff --git a/src/Simplify.String.Tests/Simplify.String.Tests.csproj b/src/Simplify.String.Tests/Simplify.String.Tests.csproj index 76a4d8a3..937bf205 100644 --- a/src/Simplify.String.Tests/Simplify.String.Tests.csproj +++ b/src/Simplify.String.Tests/Simplify.String.Tests.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 Simplify.String unit tests Simplify diff --git a/src/Simplify.String/CHANGELOG.md b/src/Simplify.String/CHANGELOG.md index 5117ce87..a4532548 100644 --- a/src/Simplify.String/CHANGELOG.md +++ b/src/Simplify.String/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [1.2.3] - 2026-06-25 + +### Changed + +- Documented `StringHelper.StripHtmlTags` as a cosmetic tag remover (simple `<.*?>` regex), explicitly not an HTML/XSS sanitizer + +### Added + +- .NET 10 explicit support + ## [1.2.2] - 2023-08-01 ### Removed diff --git a/src/Simplify.String/Simplify.String.csproj b/src/Simplify.String/Simplify.String.csproj index 8c6a643a..2a86a32f 100644 --- a/src/Simplify.String/Simplify.String.csproj +++ b/src/Simplify.String/Simplify.String.csproj @@ -1,12 +1,12 @@ - net6.0;netstandard2.1;netstandard2.0;net48 + net10.0;net6.0;netstandard2.1;netstandard2.0;net48 true true snupkg true - 1.2.2 + 1.2.3 String operations/validation functions Simplify diff --git a/src/Simplify.String/StringHelper.cs b/src/Simplify.String/StringHelper.cs index ce763c3b..034b10f0 100644 --- a/src/Simplify.String/StringHelper.cs +++ b/src/Simplify.String/StringHelper.cs @@ -95,6 +95,10 @@ public static float IndistinctMatching(string stringA, string stringB, int compa /// /// The source string. /// + /// + /// This is a cosmetic tag remover (simple <.*?> regex), not an HTML/XSS sanitizer. Do not rely on it + /// to make untrusted input safe for rendering: it does not handle malformed tags, comments, scripts or attributes. + /// public static string StripHtmlTags(string source) { return Regex.Replace(source, "<.*?>", string.Empty); diff --git a/src/Simplify.System.Tests/Simplify.System.Tests.csproj b/src/Simplify.System.Tests/Simplify.System.Tests.csproj index 95052fe9..ca71eb5e 100644 --- a/src/Simplify.System.Tests/Simplify.System.Tests.csproj +++ b/src/Simplify.System.Tests/Simplify.System.Tests.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 latest enable diff --git a/src/Simplify.System/ApplicationEnvironment.cs b/src/Simplify.System/ApplicationEnvironment.cs index a0750d90..7a9f1a67 100644 --- a/src/Simplify.System/ApplicationEnvironment.cs +++ b/src/Simplify.System/ApplicationEnvironment.cs @@ -18,6 +18,8 @@ public static class ApplicationEnvironment /// public const string EnvironmentVariableName = "ASPNETCORE_ENVIRONMENT"; + private static readonly object NameLock = new(); + private static string? _name; /// @@ -43,7 +45,16 @@ public static class ApplicationEnvironment /// public static string Name { - get { return _name ??= Environment.GetEnvironmentVariable(EnvironmentVariableName) ?? DefaultEnvironmentName; } + get + { + if (_name != null) return _name; + + lock (NameLock) + _name ??= Environment.GetEnvironmentVariable(EnvironmentVariableName) ?? DefaultEnvironmentName; + + return _name; + } + set => _name = value ?? throw new ArgumentNullException(nameof(value)); } } \ No newline at end of file diff --git a/src/Simplify.System/AssemblyInfo.cs b/src/Simplify.System/AssemblyInfo.cs index 8dcad2f3..969a6071 100644 --- a/src/Simplify.System/AssemblyInfo.cs +++ b/src/Simplify.System/AssemblyInfo.cs @@ -6,20 +6,18 @@ namespace Simplify.System; /// /// Provides the assembly information /// -public class AssemblyInfo : IAssemblyInfo +/// +/// Initializes a new instance of the class. +/// +/// The information assembly. +/// infoAssembly +public class AssemblyInfo(Assembly infoAssembly) : IAssemblyInfo { private static IAssemblyInfo? _entryAssemblyInfo; - private readonly Assembly _infoAssembly; - /// - /// Initializes a new instance of the class. - /// - /// The information assembly. - /// infoAssembly - public AssemblyInfo(Assembly infoAssembly) - { - _infoAssembly = infoAssembly ?? throw new ArgumentNullException(nameof(infoAssembly)); - } + private static readonly object _entryAssemblyInfoLock = new(); + + private readonly Assembly _infoAssembly = infoAssembly ?? throw new ArgumentNullException(nameof(infoAssembly)); /// /// Gets or sets the entry assembly information. @@ -27,7 +25,16 @@ public AssemblyInfo(Assembly infoAssembly) /// value public static IAssemblyInfo Entry { - get => _entryAssemblyInfo ??= new AssemblyInfo(Assembly.GetEntryAssembly() ?? throw new InvalidOperationException()); + get + { + if (_entryAssemblyInfo != null) + return _entryAssemblyInfo; + + lock (_entryAssemblyInfoLock) + _entryAssemblyInfo ??= new AssemblyInfo(Assembly.GetEntryAssembly() ?? throw new InvalidOperationException()); + + return _entryAssemblyInfo; + } set => _entryAssemblyInfo = value ?? throw new ArgumentNullException(nameof(value)); } @@ -42,6 +49,7 @@ public string CompanyName get { var attributes = _infoAssembly.GetCustomAttributes(typeof(AssemblyCompanyAttribute), false); + return attributes.Length == 0 ? "" : ((AssemblyCompanyAttribute)attributes[0]).Company; } } @@ -57,6 +65,7 @@ public string Copyright get { var attributes = _infoAssembly.GetCustomAttributes(typeof(AssemblyCopyrightAttribute), false); + return attributes.Length == 0 ? "" : ((AssemblyCopyrightAttribute)attributes[0]).Copyright; } } @@ -72,6 +81,7 @@ public string Description get { var attributes = _infoAssembly.GetCustomAttributes(typeof(AssemblyDescriptionAttribute), false); + return attributes.Length == 0 ? "" : ((AssemblyDescriptionAttribute)attributes[0]).Description; } } @@ -87,6 +97,7 @@ public string ProductName get { var attributes = _infoAssembly.GetCustomAttributes(typeof(AssemblyProductAttribute), false); + return attributes.Length == 0 ? "" : ((AssemblyProductAttribute)attributes[0]).Product; } } @@ -111,7 +122,7 @@ public string Title return titleAttribute.Title; } - return global::System.IO.Path.GetFileNameWithoutExtension(Assembly.GetExecutingAssembly().Location ?? throw new InvalidOperationException()); + return _infoAssembly.GetName().Name ?? throw new InvalidOperationException(); } } diff --git a/src/Simplify.System/CHANGELOG.md b/src/Simplify.System/CHANGELOG.md index 5123ed67..754b647a 100644 --- a/src/Simplify.System/CHANGELOG.md +++ b/src/Simplify.System/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [1.6.3] - 2026-06-25 + +### Added + +- .NET 10 support + +### Fixed + +- `WeakSingleton` was not fully thread-safe: the lock-free `TryGetTarget` fast path could race with target re-creation (`WeakReference` instance members are not guaranteed thread-safe); reads and writes are now fully synchronized under a single lock +- `ApplicationEnvironment.Name`, `TimeProvider.Current`, `AssemblyInfo.Entry` lazy initialization was not thread-safe (`??=`) and could return different instances under concurrent first access; initialization is now synchronized with double-checked locking +- `ObjectConverter` parameterless protected constructor left `ConvertFunc` null; `Convert` now guards against null with a descriptive exception +- `BytesExtensions.GetString` silently dropped the last byte of an odd-length array; it now throws `ArgumentNullException`/`ArgumentException` instead of returning truncated data +- `AssemblyInfo.Title` fallback resolved the name of the executing assembly (`Simplify.System`) instead of the wrapped assembly; it now returns the wrapped assembly name and no longer relies on `Assembly.Location` (empty in single-file/AOT) +- `DateTimeExtensions.TrimMilliseconds` reset `DateTimeKind` to `Unspecified`; it now preserves the original `Utc`/`Local`/`Unspecified` kind + ## [1.6.2] - 2023-08-02 ### Removed diff --git a/src/Simplify.System/Converters/ChainedObjectConverter.cs b/src/Simplify.System/Converters/ChainedObjectConverter.cs index 58053c7e..e5872f3f 100644 --- a/src/Simplify.System/Converters/ChainedObjectConverter.cs +++ b/src/Simplify.System/Converters/ChainedObjectConverter.cs @@ -27,7 +27,7 @@ public ChainedObjectConverter(Func convertFunc, Func - /// Creates instance of ChainedObjectConverter with uninitialized ConvertFunc + /// Creates instance of ChainedObjectConverter with deferred ConvertFunc initialization /// /// protected ChainedObjectConverter(Func? preConvertFunc = null) diff --git a/src/Simplify.System/Converters/ObjectConverter.cs b/src/Simplify.System/Converters/ObjectConverter.cs index 66a70456..e4fc9a4b 100644 --- a/src/Simplify.System/Converters/ObjectConverter.cs +++ b/src/Simplify.System/Converters/ObjectConverter.cs @@ -19,7 +19,7 @@ public ObjectConverter(Func convertFunc) } /// - /// Creates instance of ObjectConverter with uninitialized ConvertFunc + /// Creates instance of ObjectConverter with uninitialized ConvertFunc (for subclass deferred initialization) /// protected ObjectConverter() { diff --git a/src/Simplify.System/Extensions/BytesExtensions.cs b/src/Simplify.System/Extensions/BytesExtensions.cs index bce76853..bcba232c 100644 --- a/src/Simplify.System/Extensions/BytesExtensions.cs +++ b/src/Simplify.System/Extensions/BytesExtensions.cs @@ -12,8 +12,17 @@ public static class BytesExtensions /// /// The bytes array. /// + /// bytes + /// The array length is odd and cannot be converted to a string without losing data. public static string GetString(this byte[] bytes) { + if (bytes == null) + throw new ArgumentNullException(nameof(bytes)); + + // An odd-length array can't be mapped to whole UTF-16 chars; fail loudly instead of silently dropping the last byte. + if (bytes.Length % sizeof(char) != 0) + throw new ArgumentException("Byte array length must be even to be converted to a string.", nameof(bytes)); + var chars = new char[bytes.Length / sizeof(char)]; Buffer.BlockCopy(bytes, 0, chars, 0, bytes.Length); return new string(chars); diff --git a/src/Simplify.System/Extensions/DateTimeExtensions.cs b/src/Simplify.System/Extensions/DateTimeExtensions.cs index db18163c..efc31605 100644 --- a/src/Simplify.System/Extensions/DateTimeExtensions.cs +++ b/src/Simplify.System/Extensions/DateTimeExtensions.cs @@ -14,6 +14,7 @@ public static class DateTimeExtensions /// public static DateTime TrimMilliseconds(this DateTime dt) { - return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, 0); + // Preserve the original DateTimeKind (Utc/Local/Unspecified); the millisecond-only overload would reset it to Unspecified. + return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, dt.Kind); } } \ No newline at end of file diff --git a/src/Simplify.System/Simplify.System.csproj b/src/Simplify.System/Simplify.System.csproj index 0b03f1b7..21f0d114 100644 --- a/src/Simplify.System/Simplify.System.csproj +++ b/src/Simplify.System/Simplify.System.csproj @@ -1,6 +1,6 @@ - net6.0;netstandard2.1;netstandard2.0;net48 + net10.0;net6.0;netstandard2.1;netstandard2.0;net48 latest enable false @@ -10,7 +10,7 @@ snupkg true - 1.6.2 + 1.6.3 Core classes for applications and libraries Simplify diff --git a/src/Simplify.System/TimeProvider.cs b/src/Simplify.System/TimeProvider.cs index 6d49d253..a29cbac7 100644 --- a/src/Simplify.System/TimeProvider.cs +++ b/src/Simplify.System/TimeProvider.cs @@ -7,6 +7,8 @@ namespace Simplify.System; /// public static class TimeProvider { + private static readonly object _currentInstanceLock = new(); + private static ITimeProvider? _currentInstance; /// @@ -18,7 +20,16 @@ public static class TimeProvider /// value public static ITimeProvider Current { - get => _currentInstance ??= new SystemTimeProvider(); + get + { + if (_currentInstance != null) return _currentInstance; + + lock (_currentInstanceLock) + _currentInstance ??= new SystemTimeProvider(); + + return _currentInstance; + } + set => _currentInstance = value ?? throw new ArgumentNullException(nameof(value)); } } \ No newline at end of file diff --git a/src/Simplify.System/WeakSingleton.cs b/src/Simplify.System/WeakSingleton.cs index 920870d4..56a6cd80 100644 --- a/src/Simplify.System/WeakSingleton.cs +++ b/src/Simplify.System/WeakSingleton.cs @@ -6,19 +6,16 @@ namespace Simplify.System; /// Provides Singleton implemented using WeakReference /// /// -public class WeakSingleton where T : class +/// +/// Creates instance of WeakSingleton +/// +/// Builder function. If null, uses default constructor. +public class WeakSingleton(Func? typeBuilder = null) where T : class { - private readonly Func _typeBuilder; - private WeakReference _ref = new(null!); + private readonly Func _typeBuilder = typeBuilder ?? (() => (T)Activator.CreateInstance(typeof(T), true)!); + private readonly WeakReference _ref = new(null!); - /// - /// Creates instance of WeakSingleton - /// - /// Builder function. If null, uses default constructor. - public WeakSingleton(Func? typeBuilder = null) - { - _typeBuilder = typeBuilder ?? (() => (T)Activator.CreateInstance(typeof(T), true)!); - } + private readonly object _locker = new(); /// /// Gets the instance of type T @@ -27,12 +24,18 @@ public T Instance { get { - if (_ref.TryGetTarget(out var target)) - return target; + // WeakReference<T> instance members are not guaranteed to be thread-safe, so both the + // read (TryGetTarget) and the write (SetTarget) are performed under the same lock to avoid + // a torn state when one thread reads while another re-creates the collected target. + lock (_locker) + { + if (_ref.TryGetTarget(out var target)) + return target; - target = _typeBuilder(); - _ref.SetTarget(target); - return target; + target = _typeBuilder(); + _ref.SetTarget(target); + return target; + } } } @@ -40,8 +43,5 @@ public T Instance /// Implicitly converts WeakSingleton to type T /// /// - public static implicit operator T(WeakSingleton weak) - { - return weak.Instance; - } + public static implicit operator T(WeakSingleton weak) => weak.Instance; } \ No newline at end of file diff --git a/src/Simplify.Templates.Tests/Simplify.Templates.Tests.csproj b/src/Simplify.Templates.Tests/Simplify.Templates.Tests.csproj index d9d3cb32..943dc030 100644 --- a/src/Simplify.Templates.Tests/Simplify.Templates.Tests.csproj +++ b/src/Simplify.Templates.Tests/Simplify.Templates.Tests.csproj @@ -1,6 +1,6 @@ - net9.0;net48 + net10.0;net48 latest Simplify.Templates unit tests diff --git a/src/Simplify.Templates/CHANGELOG.md b/src/Simplify.Templates/CHANGELOG.md index 3592d3a0..3a56a251 100644 --- a/src/Simplify.Templates/CHANGELOG.md +++ b/src/Simplify.Templates/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [2.0.3] - 2026-06-25 + +### Added + +- .NET 10 support + +### Fixed + +- `TemplateBuilder.BuildAsync` ignored the preprocessing result, so `FixLineEndingsHtml` had no effect in the asynchronous build path + ### [2.0.2] - 2024-05-25 ### Removed diff --git a/src/Simplify.Templates/Simplify.Templates.csproj b/src/Simplify.Templates/Simplify.Templates.csproj index 74a3daef..720d06bd 100644 --- a/src/Simplify.Templates/Simplify.Templates.csproj +++ b/src/Simplify.Templates/Simplify.Templates.csproj @@ -1,6 +1,6 @@ - net6.0;netstandard2.1;netstandard2.0;net48 + net10.0;net6.0;netstandard2.1;netstandard2.0;net48 latest enable true @@ -8,7 +8,7 @@ snupkg true - 2.0.2 + 2.0.3 Text templates engine Simplify diff --git a/src/Simplify.Templates/TemplateBuilder.cs b/src/Simplify.Templates/TemplateBuilder.cs index dc0acf05..a29372ab 100644 --- a/src/Simplify.Templates/TemplateBuilder.cs +++ b/src/Simplify.Templates/TemplateBuilder.cs @@ -143,7 +143,7 @@ public async Task BuildAsync() { var text = await LoadTemplateTextAsync(); - PreprocessTemplateText(text); + text = PreprocessTemplateText(text); var tpl = new Template(text); diff --git a/src/Simplify.WindowsServices/CHANGELOG.md b/src/Simplify.WindowsServices/CHANGELOG.md index f2918d79..97d040a5 100644 --- a/src/Simplify.WindowsServices/CHANGELOG.md +++ b/src/Simplify.WindowsServices/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [2.16] - 2026-06-25 + +### Fixed + +- Crontab job timers were never stopped/disposed on service stop/dispose, leaking timers that kept firing during and after shutdown; all job timers are now stopped before waiting for running tasks and on dispose +- Data race in `CrontabProcessor` between `IsMatching` and `CalculateNextOccurrences` over the shared occurrences list; access is now synchronized +- Lifetime scope leak in basic job execution when resolving or invoking the job throws; the scope is now disposed on failure +- Exceptions thrown from the crontab timer callback (`OnCronTimerTick`/`OnStartWork`) escaped to a thread-pool thread and terminated the service process; they are now routed to the `OnException` event +- Unhandled job exceptions were rethrown from inside the worker task, surfacing as an `AggregateException` from `Task.WaitAll` and aborting graceful shutdown; they are no longer rethrown +- Basic (long-running) job blocked `OnStart` (causing Service Control Manager start timeouts) and its lifetime scope was registered for disposal only after the job method returned, leaking the scope; the job now runs as a background task and the scope is registered before it runs + +#### Dependencies + +- Microsoft.Extensions.Configuration bump to 10.0.9 + ## [2.15.1] - 2025-10-10 ### Dependencies diff --git a/src/Simplify.WindowsServices/Jobs/Crontab/CrontabProcessor.cs b/src/Simplify.WindowsServices/Jobs/Crontab/CrontabProcessor.cs index b13a0d6c..37a9cc27 100644 --- a/src/Simplify.WindowsServices/Jobs/Crontab/CrontabProcessor.cs +++ b/src/Simplify.WindowsServices/Jobs/Crontab/CrontabProcessor.cs @@ -11,6 +11,8 @@ namespace Simplify.WindowsServices.Jobs.Crontab; /// public class CrontabProcessor : ICrontabProcessor { + private readonly object _occurrencesLocker = new object(); + /// /// Initializes a new instance of the class. /// @@ -66,10 +68,13 @@ public CrontabProcessor(string crontabExpression) /// The base time. public void CalculateNextOccurrences(DateTime baseTime) { - NextOccurrences.Clear(); + lock (_occurrencesLocker) + { + NextOccurrences.Clear(); - foreach (var schedule in Schedules) - NextOccurrences.Add(schedule.GetNextOccurrence(baseTime)); + foreach (var schedule in Schedules) + NextOccurrences.Add(schedule.GetNextOccurrence(baseTime)); + } } /// @@ -79,11 +84,12 @@ public void CalculateNextOccurrences(DateTime baseTime) /// public bool IsMatching(DateTime time) { - return - NextOccurrences.Any( - occurrence => - time.Year == occurrence.Year && time.Month == occurrence.Month && time.Day == occurrence.Day && - time.Hour == occurrence.Hour && time.Minute == occurrence.Minute); + lock (_occurrencesLocker) + return + NextOccurrences.Any( + occurrence => + time.Year == occurrence.Year && time.Month == occurrence.Month && time.Day == occurrence.Day && + time.Hour == occurrence.Hour && time.Minute == occurrence.Minute); } /// diff --git a/src/Simplify.WindowsServices/MultitaskServiceHandler.cs b/src/Simplify.WindowsServices/MultitaskServiceHandler.cs index c4dcc947..05b27f7e 100644 --- a/src/Simplify.WindowsServices/MultitaskServiceHandler.cs +++ b/src/Simplify.WindowsServices/MultitaskServiceHandler.cs @@ -204,12 +204,23 @@ public bool Start(string[]? args = null) protected override void Dispose(bool disposing) { if (disposing) - foreach (var basicJobItem in _workingBasicJobs) + { + // Stop and dispose all job timers (idempotent if already stopped in OnStop) to avoid leaking them. + foreach (var job in _jobs) + job.Stop(); + + lock (_workingBasicJobs) { - OnJobFinish?.Invoke(basicJobItem.Key); + foreach (var basicJobItem in _workingBasicJobs) + { + OnJobFinish?.Invoke(basicJobItem.Key); + + basicJobItem.Value.Dispose(); + } - basicJobItem.Value.Dispose(); + _workingBasicJobs.Clear(); } + } base.Dispose(disposing); } @@ -225,7 +236,7 @@ protected override void OnStart(string[] args) job.Start(); if (!(job is ICrontabServiceJob)) - RunBasicJob(job).Wait(); + RunBasicJob(job); } base.OnStart(args); @@ -237,6 +248,11 @@ protected override void OnStart(string[] args) protected override void OnStop() { ShutdownInProcess = true; + + // Stop all job timers first so no new tasks are spawned while we wait for the running ones to finish. + foreach (var job in _jobs) + job.Stop(); + Task[] itemsToWait; lock (_workingJobsTasks) @@ -257,33 +273,55 @@ private void InitializeJob(ICrontabServiceJob job) private void OnCronTimerTick(object state) { - var job = (ICrontabServiceJob)state; + try + { + var job = (ICrontabServiceJob)state; - if (job.CrontabProcessor == null) - throw new InvalidOperationException($"{nameof(job.CrontabProcessor)} is null"); + if (job.CrontabProcessor == null) + throw new InvalidOperationException($"{nameof(job.CrontabProcessor)} is null"); - if (!job.CrontabProcessor.IsMatching()) - return; + if (!job.CrontabProcessor.IsMatching()) + return; - job.CrontabProcessor.CalculateNextOccurrences(); + job.CrontabProcessor.CalculateNextOccurrences(); - OnStartWork(state); + OnStartWork(state); + } + catch (Exception e) + { + RaiseOnExceptionOrTrace(e); + } } private void OnStartWork(object state) { - var job = (ICrontabServiceJob)state; - - lock (_workingJobsTasks) + try { - if (ShutdownInProcess || _workingJobsTasks.Count(x => x.Job == job) >= job.Settings.MaximumParallelTasksCount) - return; + var job = (ICrontabServiceJob)state; + + lock (_workingJobsTasks) + { + if (ShutdownInProcess || _workingJobsTasks.Count(x => x.Job == job) >= job.Settings.MaximumParallelTasksCount) + return; - _jobTaskID++; + _jobTaskID++; - _workingJobsTasks.Add(new CrontabServiceJobTask(_jobTaskID, job, - Task.Factory.StartNew(Run, new Tuple(_jobTaskID, job)).Unwrap())); + _workingJobsTasks.Add(new CrontabServiceJobTask(_jobTaskID, job, + Task.Factory.StartNew(Run, new Tuple(_jobTaskID, job)).Unwrap())); + } } + catch (Exception e) + { + RaiseOnExceptionOrTrace(e); + } + } + + private void RaiseOnExceptionOrTrace(Exception e) + { + if (OnException != null) + OnException(new ServiceExceptionArgs(ServiceName, e)); + else + Console.Error.WriteLine($"Unhandled exception in windows service job: {e}"); } #region Execution @@ -302,10 +340,7 @@ private async Task Run(object state) } catch (Exception e) { - if (OnException != null) - OnException(new ServiceExceptionArgs(ServiceName, e)); - else - throw; + RaiseOnExceptionOrTrace(e); } finally { @@ -326,26 +361,51 @@ private async Task RunScoped(IServiceJobRepresentation job) OnJobFinish?.Invoke(job); } - private async Task RunBasicJob(IServiceJobRepresentation job) + private void RunBasicJob(IServiceJobRepresentation job) { + var scope = DIContainer.Current.BeginLifetimeScope(); + + object jobObject; + try { - var scope = DIContainer.Current.BeginLifetimeScope(); - - var jobObject = scope.Resolver.Resolve(job.JobClassType); + jobObject = scope.Resolver.Resolve(job.JobClassType); OnJobStart?.Invoke(job); + } + catch (Exception e) + { + scope.Dispose(); - await InvokeJobMethod(job, jobObject); + RaiseOnExceptionOrTrace(e); - _workingBasicJobs.Add(job, scope); + return; } - catch (Exception e) + + // The scope must be registered for disposal before awaiting: basic jobs are typically long-running + // (server-style) and the invocation may never complete, so registering it after the await would leak the scope. + lock (_workingBasicJobs) + _workingBasicJobs.Add(job, scope); + + // Basic jobs must not block OnStart (the SCM would time out waiting for the service to start), + // so the (possibly non-completing) job method is run as a background task. + _ = InvokeJobMethod(job, jobObject).ContinueWith(t => { - if (OnException != null) - OnException(new ServiceExceptionArgs(ServiceName, e)); - else - throw; + RemoveAndDisposeBasicJob(job); + + RaiseOnExceptionOrTrace(t.Exception!.GetBaseException()); + }, TaskContinuationOptions.OnlyOnFaulted); + } + + private void RemoveAndDisposeBasicJob(IServiceJobRepresentation job) + { + lock (_workingBasicJobs) + { + if (!_workingBasicJobs.TryGetValue(job, out var scope)) + return; + + scope.Dispose(); + _workingBasicJobs.Remove(job); } } diff --git a/src/Simplify.WindowsServices/Simplify.WindowsServices.csproj b/src/Simplify.WindowsServices/Simplify.WindowsServices.csproj index 55036259..0c11d668 100644 --- a/src/Simplify.WindowsServices/Simplify.WindowsServices.csproj +++ b/src/Simplify.WindowsServices/Simplify.WindowsServices.csproj @@ -8,7 +8,7 @@ snupkg true - 2.15.1 + 2.16.0 Windows services framework with DI Simplify @@ -27,7 +27,7 @@ - + diff --git a/src/Simplify.Xml.Tests/Simplify.Xml.Tests.csproj b/src/Simplify.Xml.Tests/Simplify.Xml.Tests.csproj index 23963c95..759c1173 100644 --- a/src/Simplify.Xml.Tests/Simplify.Xml.Tests.csproj +++ b/src/Simplify.Xml.Tests/Simplify.Xml.Tests.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 Simplify.Xml unit tests Simplify diff --git a/src/Simplify.Xml/CHANGELOG.md b/src/Simplify.Xml/CHANGELOG.md index 91d4f86c..20b7c49d 100644 --- a/src/Simplify.Xml/CHANGELOG.md +++ b/src/Simplify.Xml/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [1.4.1] - 2026-06-25 + +### Added + +- .NET 10 support + +### Changed + +- `RemoveAllXmlNamespaces` now removes all namespace declarations in a single regex pass instead of an O(n²) per-match string replacement loop (same result) +- Duplicate converter classes (`IObjectConverter`, `ObjectConverter`, `ChainedObjectConverter`) removed from Simplify.Xml; project now references Simplify.System converters to eliminate DRY violation + ## [1.4.0] - 2024-05-25 ### Changed diff --git a/src/Simplify.Xml/Converters/ChainedObjectConverter.cs b/src/Simplify.Xml/Converters/ChainedObjectConverter.cs deleted file mode 100644 index 83de5b71..00000000 --- a/src/Simplify.Xml/Converters/ChainedObjectConverter.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; - -namespace Simplify.Xml.Converters; - -/// -/// Provides customizable object-to-object converter that can be chained with similar converters -/// -/// -/// -public class ChainedObjectConverter : ObjectConverter - where TDestination : TSource -{ - /// - /// Func delegate that converts source to destination before the main converter delegate - /// - protected readonly Func? PreConvertFunc; - - /// - /// Creates instance of ChainedObjectConverter - /// - /// Func delegate that converts source to destination - /// Func delegate that converts source to destination before the main converter delegate - public ChainedObjectConverter(Func convertFunc, Func? preConvertFunc = null) - : base(convertFunc) - { - PreConvertFunc = preConvertFunc; - } - - /// - /// Creates instance of ChainedObjectConverter with uninitialized ConvertFunc - /// - /// - protected ChainedObjectConverter(Func? preConvertFunc = null) - { - PreConvertFunc = preConvertFunc; - } - - /// - /// Converts source object to destination object - /// - /// Source object - /// Destination object - public override TDestination Convert(TSource source) - { - return PreConvertFunc is null - ? base.Convert(source) - : base.Convert(PreConvertFunc(source)); - } -} \ No newline at end of file diff --git a/src/Simplify.Xml/Converters/IObjectConverter.cs b/src/Simplify.Xml/Converters/IObjectConverter.cs deleted file mode 100644 index 4fbb5181..00000000 --- a/src/Simplify.Xml/Converters/IObjectConverter.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; - -namespace Simplify.Xml.Converters; - -/// -/// Defines interface for simple object-to-object converter -/// -/// Source type -/// Destination type -public interface IObjectConverter -{ - /// - /// Converts source object to destination object - /// - /// Source object - /// Destination object - TDestination Convert(TSource source); - - /// - /// Provides Convert method as Func delegate - /// - /// Func delegate - Func AsFunc(); -} \ No newline at end of file diff --git a/src/Simplify.Xml/Converters/ObjectConverter.cs b/src/Simplify.Xml/Converters/ObjectConverter.cs deleted file mode 100644 index 6f1d2878..00000000 --- a/src/Simplify.Xml/Converters/ObjectConverter.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; - -namespace Simplify.Xml.Converters; - -/// -/// Provides customizable object-to-object converter -/// -/// -/// -public class ObjectConverter : IObjectConverter -{ - /// - /// Creates instance of ObjectConverter - /// - /// Func delegate that converts source to destination - public ObjectConverter(Func convertFunc) - { - ConvertFunc = convertFunc ?? throw new ArgumentNullException(nameof(convertFunc), "Convert delegate cannot be null"); - } - - /// - /// Creates instance of ObjectConverter with uninitialized ConvertFunc - /// - protected ObjectConverter() - { - } - - /// - /// Func delegate that converts source to destination - /// - protected Func? ConvertFunc { get; set; } - - /// - /// Implicitly provides Convert method as Func delegate - /// - /// - public static implicit operator Func(ObjectConverter converter) - { - return converter.AsFunc(); - } - - /// - /// Converts source object to destination object - /// - /// Source object - /// Destination object - public virtual TDestination Convert(TSource source) - { - return ConvertFunc is null - ? throw new ArgumentNullException(nameof(ConvertFunc), "Convert delegate is null") - : ConvertFunc(source); - } - - /// - /// Provides Convert method as Func delegate - /// - /// Func delegate - public virtual Func AsFunc() - { - return Convert; - } -} \ No newline at end of file diff --git a/src/Simplify.Xml/Simplify.Xml.csproj b/src/Simplify.Xml/Simplify.Xml.csproj index 7b9533d6..6eb2a168 100644 --- a/src/Simplify.Xml/Simplify.Xml.csproj +++ b/src/Simplify.Xml/Simplify.Xml.csproj @@ -1,6 +1,6 @@ - net6.0;netstandard2.1;netstandard2.0;net48 + net10.0;net6.0;netstandard2.1;netstandard2.0;net48 latest enable false @@ -10,7 +10,7 @@ snupkg true - 1.4.0 + 1.4.1 XML extensions Simplify @@ -24,6 +24,9 @@ See https://github.com/SimplifyNet/Simplify/tree/master/src/Simplify.Xml/CHANGELOG.md for details + + + diff --git a/src/Simplify.Xml/XNodePathConverter.cs b/src/Simplify.Xml/XNodePathConverter.cs index 7e95d743..9334ed2e 100644 --- a/src/Simplify.Xml/XNodePathConverter.cs +++ b/src/Simplify.Xml/XNodePathConverter.cs @@ -1,7 +1,7 @@ using System; using System.Xml; using System.Xml.Linq; -using Simplify.Xml.Converters; +using Simplify.System.Converters; namespace Simplify.Xml; diff --git a/src/Simplify.Xml/XmlExtensions.cs b/src/Simplify.Xml/XmlExtensions.cs index a0a0cd93..f6f5a94b 100644 --- a/src/Simplify.Xml/XmlExtensions.cs +++ b/src/Simplify.Xml/XmlExtensions.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Generic; using System.Text.RegularExpressions; using System.Xml; using System.Xml.Linq; @@ -100,16 +98,7 @@ public static string OuterXml(this XNode element) public static string RemoveAllXmlNamespaces(this string xmlData) { const string xmlnsPattern = "\\s+xmlns\\s*(:\\w)?\\s*=\\s*\\\"(?[^\\\"]*)\\\""; - var matchCollection = Regex.Matches(xmlData, xmlnsPattern); - foreach (var m in matchCollection.Cast()) - { - if (m == null) - throw new InvalidOperationException("Match collection item is null"); - - xmlData = xmlData.Replace(m.ToString(), ""); - } - - return xmlData; + return Regex.Replace(xmlData, xmlnsPattern, ""); } } \ No newline at end of file diff --git a/src/Simplify.sln.DotSettings b/src/Simplify.sln.DotSettings deleted file mode 100644 index 420cc597..00000000 --- a/src/Simplify.sln.DotSettings +++ /dev/null @@ -1,25 +0,0 @@ - - DI - ID - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True \ No newline at end of file