diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3656fd0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,203 @@ +[*] +charset = utf-8-bom +end_of_line = crlf +trim_trailing_whitespace = false +insert_final_newline = false +indent_style = space +indent_size = 4 + +# Microsoft .NET properties +csharp_new_line_before_members_in_object_initializers = false +csharp_new_line_before_open_brace = none +csharp_preferred_modifier_order = public, internal, protected, private, static, async, virtual, file, new, sealed, override, required, abstract, extern, volatile, unsafe, readonly:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_var_elsewhere = false:none +csharp_style_var_for_built_in_types = false:suggestion +dotnet_naming_rule.private_constants_rule.import_to_resharper = True +dotnet_naming_rule.private_constants_rule.resharper_description = Constant fields (private) +dotnet_naming_rule.private_constants_rule.resharper_guid = 236f7aa5-7b06-43ca-bf2a-9b31bfcff09a +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 = True +dotnet_naming_rule.private_instance_fields_rule.resharper_description = Instance fields (private) +dotnet_naming_rule.private_instance_fields_rule.resharper_guid = 4a98fdf6-7d98-4f5a-afeb-ea44ad98c70c +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_instance_fields_rule_1.import_to_resharper = True +dotnet_naming_rule.private_instance_fields_rule_1.resharper_description = Instance fields (private) +dotnet_naming_rule.private_instance_fields_rule_1.resharper_guid = 4a98fdf6-7d98-4f5a-afeb-ea44ad98c70c +dotnet_naming_rule.private_instance_fields_rule_1.severity = warning +dotnet_naming_rule.private_instance_fields_rule_1.style = lower_camel_case_style +dotnet_naming_rule.private_instance_fields_rule_1.symbols = private_instance_fields_symbols_1 +dotnet_naming_rule.private_static_fields_rule.import_to_resharper = True +dotnet_naming_rule.private_static_fields_rule.resharper_description = Static fields (private) +dotnet_naming_rule.private_static_fields_rule.resharper_exclusive_prefixes_suffixes = true +dotnet_naming_rule.private_static_fields_rule.resharper_guid = f9fce829-e6f4-4cb2-80f1-5497c44f51df +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 = True +dotnet_naming_rule.private_static_readonly_rule.resharper_description = Static readonly fields (private) +dotnet_naming_rule.private_static_readonly_rule.resharper_guid = 15b5b1f1-457c-4ca6-b278-5615aedc07d3 +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.unity_serialized_field_rule.import_to_resharper = True +dotnet_naming_rule.unity_serialized_field_rule.resharper_description = Unity serialized field +dotnet_naming_rule.unity_serialized_field_rule.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef +dotnet_naming_rule.unity_serialized_field_rule.severity = warning +dotnet_naming_rule.unity_serialized_field_rule.style = lower_camel_case_style_1 +dotnet_naming_rule.unity_serialized_field_rule.symbols = unity_serialized_field_symbols +dotnet_naming_rule.unity_serialized_field_rule_1.import_to_resharper = True +dotnet_naming_rule.unity_serialized_field_rule_1.resharper_description = Unity serialized field +dotnet_naming_rule.unity_serialized_field_rule_1.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef +dotnet_naming_rule.unity_serialized_field_rule_1.severity = warning +dotnet_naming_rule.unity_serialized_field_rule_1.style = lower_camel_case_style_1 +dotnet_naming_rule.unity_serialized_field_rule_1.symbols = unity_serialized_field_symbols_1 +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.upper_camel_case_style.capitalization = pascal_case +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_constants_symbols.resharper_applicable_kinds = constant_field +dotnet_naming_symbols.private_constants_symbols.resharper_required_modifiers = any +dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.private_instance_fields_symbols.resharper_applicable_kinds = field, readonly_field +dotnet_naming_symbols.private_instance_fields_symbols.resharper_required_modifiers = instance +dotnet_naming_symbols.private_instance_fields_symbols_1.applicable_accessibilities = private +dotnet_naming_symbols.private_instance_fields_symbols_1.applicable_kinds = field +dotnet_naming_symbols.private_instance_fields_symbols_1.resharper_applicable_kinds = field,readonly_field +dotnet_naming_symbols.private_instance_fields_symbols_1.resharper_required_modifiers = instance +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_fields_symbols.resharper_applicable_kinds = field +dotnet_naming_symbols.private_static_fields_symbols.resharper_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 = readonly, static +dotnet_naming_symbols.private_static_readonly_symbols.resharper_applicable_kinds = readonly_field +dotnet_naming_symbols.private_static_readonly_symbols.resharper_required_modifiers = static +dotnet_naming_symbols.unity_serialized_field_symbols.applicable_accessibilities = * +dotnet_naming_symbols.unity_serialized_field_symbols.applicable_kinds = +dotnet_naming_symbols.unity_serialized_field_symbols.resharper_applicable_kinds = unity_serialised_field +dotnet_naming_symbols.unity_serialized_field_symbols.resharper_required_modifiers = instance +dotnet_naming_symbols.unity_serialized_field_symbols_1.applicable_accessibilities = * +dotnet_naming_symbols.unity_serialized_field_symbols_1.applicable_kinds = +dotnet_naming_symbols.unity_serialized_field_symbols_1.resharper_applicable_kinds = unity_serialised_field +dotnet_naming_symbols.unity_serialized_field_symbols_1.resharper_required_modifiers = instance +dotnet_sort_system_directives_first = false +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity: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 + +# ReSharper properties +resharper_align_multiline_binary_expressions_chain = false +resharper_align_multiline_statement_conditions = false +resharper_arguments_anonymous_function = named +resharper_arguments_skip_single = true +resharper_autodetect_indent_settings = true +resharper_blank_lines_after_control_transfer_statements = 1 +resharper_blank_lines_after_file_scoped_namespace_directive = 0 +resharper_blank_lines_after_start_comment = 0 +resharper_blank_lines_around_auto_property = 0 +resharper_blank_lines_around_block_case_section = 1 +resharper_blank_lines_around_property = 0 +resharper_braces_for_for = required +resharper_braces_for_foreach = required_for_multiline_statement +resharper_braces_for_ifelse = not_required +resharper_braces_for_while = required_for_multiline +resharper_braces_redundant = false +resharper_cpp_insert_final_newline = true +resharper_csharp_blank_lines_around_field = 0 +resharper_csharp_blank_lines_around_invocable = 0 +resharper_csharp_blank_lines_around_region = 0 +resharper_csharp_blank_lines_inside_region = 0 +resharper_csharp_insert_final_newline = true +resharper_csharp_keep_blank_lines_in_declarations = 100 +resharper_csharp_max_line_length = 505 +resharper_csharp_remove_blank_lines_near_braces_in_code = false +resharper_csharp_remove_blank_lines_near_braces_in_declarations = false +resharper_csharp_space_around_alias_eq = false +resharper_csharp_space_before_trailing_comment = false +resharper_csharp_stick_comment = false +resharper_csharp_wrap_after_declaration_lpar = true +resharper_csharp_wrap_before_binary_opsign = true +resharper_csharp_wrap_before_declaration_rpar = true +resharper_csharp_wrap_extends_list_style = chop_if_long +resharper_formatter_off_tag = @formatter:off +resharper_formatter_on_tag = @formatter:on +resharper_formatter_tags_enabled = true +resharper_indent_preprocessor_if = usual_indent +resharper_indent_preprocessor_other = do_not_change +resharper_indent_raw_literal_string = indent +resharper_instance_members_qualify_declared_in = +resharper_keep_existing_attribute_arrangement = true +resharper_keep_existing_declaration_block_arrangement = true +resharper_keep_existing_embedded_block_arrangement = true +resharper_max_attribute_length_for_same_line = 70 +resharper_method_or_operator_body = expression_body +resharper_parentheses_redundancy_style = remove +resharper_place_accessorholder_attribute_on_same_line = false +resharper_place_expr_accessor_on_single_line = true +resharper_place_expr_property_on_single_line = true +resharper_place_simple_accessor_on_single_line = false +resharper_place_simple_anonymousmethod_on_single_line = false +resharper_place_simple_case_statement_on_same_line = true +resharper_place_simple_embedded_statement_on_same_line = true +resharper_place_simple_initializer_on_single_line = false +resharper_place_simple_list_pattern_on_single_line = false +resharper_place_type_attribute_on_same_line = if_owner_is_single_line +resharper_qualified_using_at_nested_scope = true +resharper_space_within_empty_braces = false +resharper_treat_case_statement_with_break_as_simple = false +resharper_use_indent_from_vs = false +resharper_wrap_list_pattern = chop_always +resharper_wrap_object_and_collection_initializer_style = chop_always + +# ReSharper inspection severities +resharper_arrange_redundant_parentheses_highlighting = hint +resharper_arrange_this_qualifier_highlighting = hint +resharper_arrange_type_member_modifiers_highlighting = hint +resharper_arrange_type_modifiers_highlighting = hint +resharper_built_in_type_reference_style_for_member_access_highlighting = hint +resharper_built_in_type_reference_style_highlighting = hint +resharper_html_path_error_highlighting = none +resharper_mvc_action_not_resolved_highlighting = warning +resharper_mvc_area_not_resolved_highlighting = warning +resharper_mvc_controller_not_resolved_highlighting = warning +resharper_mvc_masterpage_not_resolved_highlighting = warning +resharper_mvc_partial_view_not_resolved_highlighting = warning +resharper_mvc_template_not_resolved_highlighting = warning +resharper_mvc_view_component_not_resolved_highlighting = warning +resharper_mvc_view_component_view_not_resolved_highlighting = warning +resharper_mvc_view_not_resolved_highlighting = warning +resharper_razor_assembly_not_resolved_highlighting = warning +resharper_redundant_base_qualifier_highlighting = warning +resharper_suggest_var_or_type_built_in_types_highlighting = hint +resharper_suggest_var_or_type_elsewhere_highlighting = hint +resharper_suggest_var_or_type_simple_types_highlighting = hint +resharper_web_config_module_not_resolved_highlighting = warning +resharper_web_config_type_not_resolved_highlighting = warning +resharper_web_config_wrong_module_highlighting = warning + +[{*.har,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,.ws-context,jest.config}] +indent_style = space +indent_size = 2 + +[*.{appxmanifest,asax,ascx,aspx,axaml,blockshader,build,c,c++,c++m,cc,ccm,cginc,compute,cp,cpp,cppm,cs,cshtml,cu,cuh,cxx,cxxm,dtd,fs,fsi,fsscript,fsx,fx,fxh,h,h++,hh,hlsl,hlsli,hlslinc,hp,hpp,hxx,icc,inc,inl,ino,ipp,ixx,master,ml,mli,mpp,mq4,mq5,mqh,mxx,nuspec,paml,razor,resw,resx,shader,shaderFoundry,skin,tcc,tpp,urtshader,usf,ush,uxml,vb,xaml,xamlx,xoml,xsd}] +indent_style = space +indent_size = 4 +tab_width = 4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a8b6ae0..bd258f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,57 +1,48 @@ name: Publish Packages on: - push: - tags: - - 'v*.*.*' # Match version tags like v1.0.0 + push: + tags: + - 'v*.*.*' # Match version tags like v1.0.0 jobs: - test: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Setup .NET - uses: actions/setup-dotnet@v3 - with: - dotnet-version: '9.x' # Adjust the version as needed - - - name: Install dependencies - run: dotnet restore - - - name: Build the solution - run: dotnet build --no-restore - - # Ensure that the tests must pass - # The job will fail automatically if any test fails because `dotnet test` exits with a non-zero code - - name: Run tests - run: dotnet test --no-build --verbosity normal - - publish: - - runs-on: ubuntu-latest - - needs: test - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Setup .NET - uses: actions/setup-dotnet@v3 - with: - dotnet-version: '9.x' - - - name: Restore dependencies - run: dotnet restore DependencyInjection.sln - - - name: Build - run: dotnet build DependencyInjection.sln --configuration Release --no-restore - - - name: Publish to NuGet - env: - NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} - run: | - dotnet nuget push src/TEMPLATE/bin/Release/*.nupkg --api-key $NUGET_API_KEY --source https://api.nuget.org/v3/index.json --skip-duplicate \ No newline at end of file + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '9.x' + + - name: Restore dependencies + run: dotnet restore CodeOfChaos.Extensions.sln + + - name: Build + run: dotnet build CodeOfChaos.Extensions.sln --configuration Release --no-restore + + # Ensure that the tests must pass + # The job will fail automatically if any test fails because `dotnet test` exits with a non-zero code + - name: Run tests - Extensions + run: dotnet run -c Release --no-restore --no-build + working-directory: "tests/Tests.CodeOfChaos.Extensions" + - name: Run tests - Extensions.AspNetCore + run: dotnet run -c Release --no-restore --no-build + working-directory: "tests/Tests.CodeOfChaos.Extensions.AspNetCore" + - name: Run tests - Extensions.EntityFrameworkCore + run: dotnet run -c Release --no-restore --no-build + working-directory: "tests/Tests.CodeOfChaos.Extensions.EntityFrameworkCore" + - name: Run tests - Extensions.Serilog + run: dotnet run -c Release --no-restore --no-build + working-directory: "tests/Tests.CodeOfChaos.Extensions.Serilog" + + - name: Publish to NuGet + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + run: | + dotnet nuget push src/CodeOfChaos.Extensions/bin/Release/*.nupkg --api-key $NUGET_API_KEY --source https://api.nuget.org/v3/index.json --skip-duplicate + dotnet nuget push src/CodeOfChaos.Extensions.AspNetCore/bin/Release/*.nupkg --api-key $NUGET_API_KEY --source https://api.nuget.org/v3/index.json --skip-duplicate + dotnet nuget push src/CodeOfChaos.Extensions.EntityFrameworkCore/bin/Release/*.nupkg --api-key $NUGET_API_KEY --source https://api.nuget.org/v3/index.json --skip-duplicate + dotnet nuget push src/CodeOfChaos.Extensions.Serilog/bin/Release/*.nupkg --api-key $NUGET_API_KEY --source https://api.nuget.org/v3/index.json --skip-duplicate \ No newline at end of file diff --git a/CodeOfChaos.Extensions.sln b/CodeOfChaos.Extensions.sln index 3bbc729..2590912 100644 --- a/CodeOfChaos.Extensions.sln +++ b/CodeOfChaos.Extensions.sln @@ -16,6 +16,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeOfChaos.Extensions.Enti EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.CodeOfChaos.Extensions.EntityFrameworkCore", "tests\Tests.CodeOfChaos.Extensions.EntityFrameworkCore\Tests.CodeOfChaos.Extensions.EntityFrameworkCore.csproj", "{0A198DE2-E404-4BC4-9C6C-A4E1B4397463}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeOfChaos.Extensions.AspNetCore", "src\CodeOfChaos.Extensions.AspNetCore\CodeOfChaos.Extensions.AspNetCore.csproj", "{53BD8191-6E89-4E6D-AD32-5613ED73C422}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.CodeOfChaos.Extensions.AspNetCore", "tests\Tests.CodeOfChaos.Extensions.AspNetCore\Tests.CodeOfChaos.Extensions.AspNetCore.csproj", "{BC0AB42E-28A5-47FC-9017-1191C6899645}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeOfChaos.Extensions.Serilog", "src\CodeOfChaos.Extensions.Serilog\CodeOfChaos.Extensions.Serilog.csproj", "{DCFDADB7-06BE-49BB-A71F-3124A48B0ECF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.CodeOfChaos.Extensions.Serilog", "tests\Tests.CodeOfChaos.Extensions.Serilog\Tests.CodeOfChaos.Extensions.Serilog.csproj", "{8670FBAC-E420-4DC6-82B1-AF0C5BF7F797}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -42,6 +50,22 @@ Global {0A198DE2-E404-4BC4-9C6C-A4E1B4397463}.Debug|Any CPU.Build.0 = Debug|Any CPU {0A198DE2-E404-4BC4-9C6C-A4E1B4397463}.Release|Any CPU.ActiveCfg = Release|Any CPU {0A198DE2-E404-4BC4-9C6C-A4E1B4397463}.Release|Any CPU.Build.0 = Release|Any CPU + {53BD8191-6E89-4E6D-AD32-5613ED73C422}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {53BD8191-6E89-4E6D-AD32-5613ED73C422}.Debug|Any CPU.Build.0 = Debug|Any CPU + {53BD8191-6E89-4E6D-AD32-5613ED73C422}.Release|Any CPU.ActiveCfg = Release|Any CPU + {53BD8191-6E89-4E6D-AD32-5613ED73C422}.Release|Any CPU.Build.0 = Release|Any CPU + {BC0AB42E-28A5-47FC-9017-1191C6899645}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC0AB42E-28A5-47FC-9017-1191C6899645}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC0AB42E-28A5-47FC-9017-1191C6899645}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC0AB42E-28A5-47FC-9017-1191C6899645}.Release|Any CPU.Build.0 = Release|Any CPU + {DCFDADB7-06BE-49BB-A71F-3124A48B0ECF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DCFDADB7-06BE-49BB-A71F-3124A48B0ECF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DCFDADB7-06BE-49BB-A71F-3124A48B0ECF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DCFDADB7-06BE-49BB-A71F-3124A48B0ECF}.Release|Any CPU.Build.0 = Release|Any CPU + {8670FBAC-E420-4DC6-82B1-AF0C5BF7F797}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8670FBAC-E420-4DC6-82B1-AF0C5BF7F797}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8670FBAC-E420-4DC6-82B1-AF0C5BF7F797}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8670FBAC-E420-4DC6-82B1-AF0C5BF7F797}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {26284571-0E09-4BAF-8C2B-DF87DCC1BA0B} = {8DD280D4-1E14-4D5E-AFE6-58DD8F079DCC} @@ -49,5 +73,9 @@ Global {ADEADD97-0AFA-4D9E-970B-9FFB932949B3} = {AF1A203C-6EF1-440E-BB3C-55B1DBFE9C19} {411473A5-2921-4758-B78C-E66BCFFE6303} = {197E72AD-DEAB-4350-AFC3-A3BB38720BF5} {0A198DE2-E404-4BC4-9C6C-A4E1B4397463} = {8DD280D4-1E14-4D5E-AFE6-58DD8F079DCC} + {53BD8191-6E89-4E6D-AD32-5613ED73C422} = {197E72AD-DEAB-4350-AFC3-A3BB38720BF5} + {BC0AB42E-28A5-47FC-9017-1191C6899645} = {8DD280D4-1E14-4D5E-AFE6-58DD8F079DCC} + {DCFDADB7-06BE-49BB-A71F-3124A48B0ECF} = {197E72AD-DEAB-4350-AFC3-A3BB38720BF5} + {8670FBAC-E420-4DC6-82B1-AF0C5BF7F797} = {8DD280D4-1E14-4D5E-AFE6-58DD8F079DCC} EndGlobalSection EndGlobal diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000..0f207d9 Binary files /dev/null and b/assets/icon.png differ diff --git a/src/CodeOfChaos.Extensions.AspNetCore/CodeOfChaos.Extensions.AspNetCore.csproj b/src/CodeOfChaos.Extensions.AspNetCore/CodeOfChaos.Extensions.AspNetCore.csproj new file mode 100644 index 0000000..0c956bf --- /dev/null +++ b/src/CodeOfChaos.Extensions.AspNetCore/CodeOfChaos.Extensions.AspNetCore.csproj @@ -0,0 +1,36 @@ + + + + net9.0 + latest + enable + enable + + + CodeOfChaos.Extensions.AspNetCore + 0.20.0-preview.0 + Anna Sas + A Library of broadly used extensions for AspNetCore + https://github.com/code-of-chaos/cs-code_of_chaos-extensions + extensions AspNetCore + true + true + true + embedded + LICENSE + README.md + icon.png + + + + + + + + + + + + + + diff --git a/src/CodeOfChaos.Extensions.AspNetCore/LoggingOverrideExtensions.cs b/src/CodeOfChaos.Extensions.AspNetCore/LoggingOverrideExtensions.cs new file mode 100644 index 0000000..1c558fd --- /dev/null +++ b/src/CodeOfChaos.Extensions.AspNetCore/LoggingOverrideExtensions.cs @@ -0,0 +1,54 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Core; + +namespace CodeOfChaos.Extensions.AspNetCore; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +public static class LoggingOverrideExtensions { + + private static readonly LoggingLevelSwitch LoggingLevelSwitch = new(); + + /// + /// Configures Serilog as the logging provider for the web application builder. + /// + /// The to configure. + /// Optional delegate to override the default . + /// The modified . + /// Thrown when the builder is null. + public static WebApplicationBuilder OverrideLoggingWithSerilog( + this WebApplicationBuilder builder, + Action? configure = null + ) { + LoggerConfiguration loggerConfig = new LoggerConfiguration() + .MinimumLevel.ControlledBy(LoggingLevelSwitch); + + configure?.Invoke(loggerConfig); + + Log.Logger = loggerConfig.CreateLogger(); + + builder.Logging.ClearProviders(); + builder.Logging.AddSerilog(Log.Logger); + builder.Services.AddSingleton(Log.Logger); + builder.Services.AddSingleton();// Ensure cleanup + + return builder; + } + + public class ApplicationShutdownLoggerCleanup : IHostedService { + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public async Task StopAsync(CancellationToken cancellationToken) { + await Log.CloseAndFlushAsync(); + } + } +} + + diff --git a/src/CodeOfChaos.Extensions.AspNetCore/README.md b/src/CodeOfChaos.Extensions.AspNetCore/README.md new file mode 100644 index 0000000..facb444 --- /dev/null +++ b/src/CodeOfChaos.Extensions.AspNetCore/README.md @@ -0,0 +1,67 @@ +# CodeOfChaos.Extensions.AspNetCore + +`CodeOfChaos.Extensions.AspNetCore` is a lightweight library that simplifies some aspects of ASP.NET Core applications. + +--- + +## Features + +### Simplified Serilog Integration +- Easily replaces the default ASP.NET Core logging with **Serilog**. +- Supports **custom configuration** via `LoggerConfiguration` for full control over your logging setup. +- Automatically manages Serilog's lifecycle, ensuring proper cleanup on application shutdown. + +--- + +## Installation + +This library targets `.NET 9.0` and requires C# 13.0. Ensure your project meets these requirements before using. + +Add the dependency to your project via NuGet: +```bash +dotnet add package CodeOfChaos.Extensions.AspNetCore +``` + +--- + +## Usage + +Here's how you can use the library to configure Serilog in your ASP.NET Core application: + +### Example Setup +1. Install the `CodeOfChaos.Extensions.AspNetCore` package in your project. +2. Modify your `Program.cs` or `Startup.cs` file: + ```csharp + using CodeOfChaos.Extensions.AspNetCore; + using Serilog; + + var builder = WebApplication.CreateBuilder(args); + + // Override logging with Serilog + builder.OverrideLoggingWithSerilog(configure: config => + { + config.WriteTo.Console(); // Example: Log to console + }); + + var app = builder.Build(); + + app.Run(); + ``` + +3. Run your application. Serilog will now be the active logging provider. + +--- + +## Features in Detail + +### Method: `OverrideLoggingWithSerilog` +This method replaces the default logging system with Serilog. It provides several key capabilities: +- Accepts an **optional configuration delegate** to customize `LoggerConfiguration` (e.g., specifying sinks like Console, File, etc.). +- Integrates all required services and clears default logging providers automatically. +- Ensures proper flushing and cleanup of logs using an internally managed hosted service on application shutdown. + +--- + +## Contributing + +Feel free to fork and contribute to the project by submitting pull requests. When contributing, ensure your changes align with the project’s coding standards. \ No newline at end of file diff --git a/src/CodeOfChaos.Extensions.EntityFrameworkCore/CodeOfChaos.Extensions.EntityFrameworkCore.csproj b/src/CodeOfChaos.Extensions.EntityFrameworkCore/CodeOfChaos.Extensions.EntityFrameworkCore.csproj index d36d0b5..a917036 100644 --- a/src/CodeOfChaos.Extensions.EntityFrameworkCore/CodeOfChaos.Extensions.EntityFrameworkCore.csproj +++ b/src/CodeOfChaos.Extensions.EntityFrameworkCore/CodeOfChaos.Extensions.EntityFrameworkCore.csproj @@ -4,10 +4,30 @@ net9.0 enable enable + + + CodeOfChaos.Extensions.EntityFrameworkCore + 0.20.0-preview.0 + Anna Sas + A Library of broadly used extensions for EntityFrameworkCore + https://github.com/code-of-chaos/cs-code_of_chaos-extensions + extensions linq EntityFrameworkCore + true + true + true + embedded + LICENSE + README.md + icon.png - + + + + + + diff --git a/src/CodeOfChaos.Extensions.EntityFrameworkCore/LinqExtensions.cs b/src/CodeOfChaos.Extensions.EntityFrameworkCore/LinqExtensions.cs index fc09ee6..24ebcf7 100644 --- a/src/CodeOfChaos.Extensions.EntityFrameworkCore/LinqExtensions.cs +++ b/src/CodeOfChaos.Extensions.EntityFrameworkCore/LinqExtensions.cs @@ -1,9 +1,10 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- -using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; + // ReSharper disable once CheckNamespace -namespace System.Linq.Expressions; +namespace Microsoft.EntityFrameworkCore; // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- @@ -14,5 +15,33 @@ public static IQueryable ConditionalInclude(this IQueryable source, boo ? source.Include(include) : source; + public static IQueryable ConditionalWhere(this IQueryable source, bool condition, Expression> predicate) => + condition + ? source.Where(predicate) + : source; + + public static IQueryable ConditionalTake(this IQueryable source, bool condition, int count) => + condition + ? source.Take(count) + : source; + + public static IQueryable ConditionalTake(this IQueryable source, bool condition, Range range) => + condition + ? source.Take(range) + : source; + public static IQueryable ConditionalOrderBy(this IQueryable source, bool condition, Expression> orderBy) => + condition + ? source.OrderBy(orderBy) + : source; + + public static IQueryable ConditionalOrderBy(this IQueryable source, bool condition, Expression> orderBy, IComparer? comparer) => + condition + ? source.OrderBy(orderBy, comparer) + : source; + + public static IQueryable ConditionalOrderByNotNull(this IQueryable source, Expression>? orderBy) => + orderBy is not null + ? source.OrderBy(orderBy) + : source; } diff --git a/src/CodeOfChaos.Extensions.EntityFrameworkCore/README.md b/src/CodeOfChaos.Extensions.EntityFrameworkCore/README.md new file mode 100644 index 0000000..dcec188 --- /dev/null +++ b/src/CodeOfChaos.Extensions.EntityFrameworkCore/README.md @@ -0,0 +1,77 @@ +# CodeOfChaos.Extensions.EntityFrameworkCore + +`CodeOfChaos.Extensions.EntityFrameworkCore` is a library that extends LINQ capabilities in Entity Framework Core. It introduces conditional extensions for operations like `Include`, `Where`, `OrderBy`, and others. These methods enable dynamic query building, improving flexibility and readability when working with Entity Framework Core. + +--- + +## Features + +### Conditional Query Building +Enhance `IQueryable` queries with conditional functionality: +- **Conditional Includes**: + - Dynamically include related entities based on runtime conditions. +- **Conditional Filters**: + - Apply `Where` clauses only when a condition is met. +- **Conditional Sorting**: + - Apply `OrderBy` clauses conditionally or use optional order expressions. +- **Conditional Pagination**: + - Apply `Take`, including support for ranges. + +--- + +## Installation + +This library targets `.NET 9.0` and requires C# 13.0. Ensure your project meets these requirements before using. + +Add the dependency to your project via NuGet: +```bash +dotnet add package CodeOfChaos.Extensions.EntityFrameworkCore +``` + +--- + +## Usage + +Here’s how you can leverage the library for dynamic query building in Entity Framework Core: + +### Conditional `Include` +Dynamically include related entities only when needed: +```csharp +using Microsoft.EntityFrameworkCore; + +var query = dbContext.Users.ConditionalInclude(isAdmin, u => u.Roles); +``` + +### Conditional `Where` +Apply a filter based on runtime conditions: +```csharp +var query = dbContext.Users.ConditionalWhere(filterByEmail, u => u.Email == searchEmail); +``` + +### Conditional `OrderBy` +Use conditional sorting: +```csharp +var query = dbContext.Users.ConditionalOrderBy(applySorting, u => u.LastName); +``` + +Or, with a comparer: +```csharp +var query = dbContext.Users.ConditionalOrderBy(applySorting, u => u.LastName, StringComparer.OrdinalIgnoreCase); +``` + +### Conditional `Take` +Limit the results dynamically: +```csharp +var query = dbContext.Users.ConditionalTake(applyPagination, 50); +``` + +Or, with a range: +```csharp +var query = dbContext.Users.ConditionalTake(applyPagination, ..10); +``` + +--- + +## Contributing + +Feel free to fork and contribute to the project by submitting pull requests. When contributing, ensure your changes align with the project’s coding standards. \ No newline at end of file diff --git a/src/CodeOfChaos.Extensions.Serilog/CodeOfChaos.Extensions.Serilog.csproj b/src/CodeOfChaos.Extensions.Serilog/CodeOfChaos.Extensions.Serilog.csproj new file mode 100644 index 0000000..58411d0 --- /dev/null +++ b/src/CodeOfChaos.Extensions.Serilog/CodeOfChaos.Extensions.Serilog.csproj @@ -0,0 +1,34 @@ + + + + net9.0 + enable + enable + + + CodeOfChaos.Extensions.Serilog + 0.20.0-preview.0 + Anna Sas + A Library of broadly used extensions for Serilog + https://github.com/code-of-chaos/cs-code_of_chaos-extensions + extensions Serilog + true + true + true + embedded + LICENSE + README.md + icon.png + + + + + + + + + + + + + diff --git a/src/CodeOfChaos.Extensions.Serilog/ExitApplicationException.cs b/src/CodeOfChaos.Extensions.Serilog/ExitApplicationException.cs new file mode 100644 index 0000000..650b0d0 --- /dev/null +++ b/src/CodeOfChaos.Extensions.Serilog/ExitApplicationException.cs @@ -0,0 +1,11 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +namespace CodeOfChaos.Extensions.Serilog; + +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +public class ExitApplicationException(int exitCode, string msg) : Exception(msg) { + public int ExitCode { get; } = exitCode; +} diff --git a/src/CodeOfChaos.Extensions.Serilog/LoggerExtensions.cs b/src/CodeOfChaos.Extensions.Serilog/LoggerExtensions.cs new file mode 100644 index 0000000..004c20d --- /dev/null +++ b/src/CodeOfChaos.Extensions.Serilog/LoggerExtensions.cs @@ -0,0 +1,107 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +using CodeOfChaos.Extensions.Serilog; +using JetBrains.Annotations; +using Serilog.Core; +using System.Diagnostics.CodeAnalysis; + +// ReSharper disable once CheckNamespace +namespace Serilog; + +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- + +/// +/// Provides extension methods for the interface. +/// +public static class LoggerExtensions { + /// + /// Throws a Error log message, logs the exception, and throws it. + /// + /// The logger instance. + /// The message template. + /// The property values. + /// Thrown exception. + [MessageTemplateFormatMethod("messageTemplate")] + public static Exception ThrowableError(this ILogger logger, string messageTemplate, params object?[]? propertyValues) { + var exception = (Exception)Activator.CreateInstance(typeof(Exception), messageTemplate)!; + logger.Error(exception, messageTemplate, propertyValues); + return exception; + } + + /// + /// Throws a Error exception and logs it using the logger. The exception is created with the specified message template + /// and property values. + /// + /// The logger instance. + /// The message template for the exception. + /// The property values for the exception. + [MessageTemplateFormatMethod("messageTemplate")] + public static TException ThrowableError(this ILogger logger, string messageTemplate, params object?[]? propertyValues) where TException : Exception, new() { + var exception = (TException)Activator.CreateInstance(typeof(TException), messageTemplate)!; + logger.Error(exception, messageTemplate, propertyValues); + return exception; + } + + /// + /// Throws a fatal log message, logs the exception, and throws it. + /// + /// The logger instance. + /// The message template. + /// The property values. + /// Thrown exception. + [MessageTemplateFormatMethod("messageTemplate")] + public static Exception ThrowableFatal(this ILogger logger, string messageTemplate, params object?[]? propertyValues) { + var exception = (Exception)Activator.CreateInstance(typeof(Exception), messageTemplate)!; + logger.Fatal(exception, messageTemplate, propertyValues); + return exception; + } + + /// + /// Throws a fatal exception and logs it using the logger. The exception is created with the specified message template + /// and property values. + /// + /// The logger instance. + /// The message template for the exception. + /// The property values for the exception. + [MessageTemplateFormatMethod("messageTemplate")] + public static TException ThrowableFatal(this ILogger logger, string messageTemplate, params object?[]? propertyValues) where TException : Exception, new() { + var exception = (TException)Activator.CreateInstance(typeof(TException), messageTemplate)!; + logger.Fatal(exception, messageTemplate, propertyValues); + return exception; + } + + /// + /// Throws a fatal exception with the specified message template and property values. + /// + /// The logger instance. + /// The message template to be used for the exception. + /// The type of expection to be thrown. + /// The property values to be used for formatting the message. + [MessageTemplateFormatMethod("messageTemplate")] + public static TException ThrowableFatal(this ILogger logger, TException exception, string messageTemplate, params object?[]? propertyValues) where TException : Exception { + logger.Fatal(exception, messageTemplate, propertyValues); + return exception; + } + + /// + /// Writes a fatal log message and exits the application with the specified exit code. + /// + /// The logger. + /// The exit code. + /// The message template. + /// The values to be included in the log message. + /// + /// This method writes a fatal log message using the specified and + /// . + /// It then exits the application with the specified . + /// + [MessageTemplateFormatMethod("messageTemplate")] + [DoesNotReturn, AssertionMethod] + public static void ExitFatal(this ILogger logger, int exitCode, string messageTemplate, params object?[]? propertyValues) { + logger.Fatal(messageTemplate, propertyValues); + throw new ExitApplicationException(exitCode, messageTemplate); + } +} diff --git a/src/CodeOfChaos.Extensions.Serilog/README.md b/src/CodeOfChaos.Extensions.Serilog/README.md new file mode 100644 index 0000000..8f033de --- /dev/null +++ b/src/CodeOfChaos.Extensions.Serilog/README.md @@ -0,0 +1,104 @@ +# CodeOfChaos.Extensions.Serilog + +`CodeOfChaos.Extensions.Serilog` extends the capabilities of Serilog, providing additional methods to simplify logging and exception management. It is particularly useful for elegantly handling fatal and error conditions, enhancing both logging clarity and application lifecycle management. + +--- + +## Features + +### Enhanced Logging and Exception Management +- **Throwable Errors**: + - Log an error and throw an exception in a single call. + - Supports both generic exceptions and custom exception types. +- **Throwable Fatals**: + - Log a fatal error and throw an exception simultaneously. + - Supports both generic and custom exceptions. +- **Exit Logging**: + - Log a fatal error and immediately terminate the application with a specified exit code. + +--- + +## Installation + +This library targets `.NET 9.0` and requires C# 13.0. Ensure your project meets these requirements before using. + +Add the dependency to your project via NuGet: +```bash +dotnet add package CodeOfChaos.Extensions.Serilog +``` + +--- + +## Usage + +Here's how you can use the library to enhance logging in your applications: + +### Throwable Errors +Log an error and throw an exception: +```csharp +using Serilog; + +ILogger logger = new LoggerConfiguration().WriteTo.Console().CreateLogger(); + +try { + throw logger.ThrowableError("An error occurred: {Error}", "SampleError"); +} catch (Exception ex) { + Console.WriteLine($"Caught exception: {ex.Message}"); +} +``` + +Or, use a specific exception type: +```csharp +try { + throw logger.ThrowableError("Invalid operation: {Details}", "SampleDetails"); +} catch (Exception ex) { + Console.WriteLine($"Caught exception: {ex.Message}"); +} +``` + +### Throwable Fatals +Log a fatal error and throw an exception: +```csharp +try { + throw logger.ThrowableFatal("A fatal error occurred: {Error}", "CriticalError"); +} catch (Exception ex) { + Console.WriteLine($"Caught fatal exception: {ex.Message}"); +} +``` + +Specify a custom exception type: +```csharp +try { + throw logger.ThrowableFatal("A critical failure: {Details}", "CriticalDetails"); +} catch (Exception ex) { + Console.WriteLine($"Caught fatal exception: {ex.Message}"); +} +``` + +### Exit Logging +Log a fatal error and immediately terminate the application with an exit code: +```csharp +logger.ExitFatal(1, "The application encountered a critical error and will exit: {Reason}", "CriticalIssue"); +``` + +--- + +## Features in Detail + +### Method: `ThrowableError` +- Logs an **error**-level message and throws an exception. +- Supports custom exception types via the generic overload `ThrowableError()`. + +### Method: `ThrowableFatal` +- Logs a **fatal**-level message and throws an exception. +- Supports custom exception types as well. + +### Method: `ExitFatal` +- Logs a **fatal**-level message and terminates the application. +- Takes an **exit code** to ensure proper command-line application management. + +--- + +## Contributing + +Feel free to fork and contribute to the project by submitting pull requests. When contributing, ensure your changes align with the project’s coding standards. diff --git a/src/CodeOfChaos.Extensions/CodeOfChaos.Extensions.csproj b/src/CodeOfChaos.Extensions/CodeOfChaos.Extensions.csproj index 514aa24..c7e5cd1 100644 --- a/src/CodeOfChaos.Extensions/CodeOfChaos.Extensions.csproj +++ b/src/CodeOfChaos.Extensions/CodeOfChaos.Extensions.csproj @@ -5,6 +5,27 @@ latest enable enable + + + CodeOfChaos.Extensions + 0.20.0-preview.0 + Anna Sas + A Library of broadly used extensions for dotnet in general + https://github.com/code-of-chaos/cs-code_of_chaos-extensions + extensions linq collections string + true + true + true + embedded + LICENSE + README.md + icon.png + + + + + + diff --git a/src/CodeOfChaos.Extensions/CollectionExtensions.cs b/src/CodeOfChaos.Extensions/CollectionExtensions.cs new file mode 100644 index 0000000..13d2f03 --- /dev/null +++ b/src/CodeOfChaos.Extensions/CollectionExtensions.cs @@ -0,0 +1,13 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +// ReSharper disable once CheckNamespace +namespace System; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +public static class CollectionExtensions { + public static bool IsEmpty(this string[] arr) => arr.Length == 0; + + public static bool IsEmpty(this IEnumerable arr) => !arr.Any(); +} diff --git a/src/CodeOfChaos.Extensions/DictionaryExtensions.cs b/src/CodeOfChaos.Extensions/DictionaryExtensions.cs new file mode 100644 index 0000000..2550786 --- /dev/null +++ b/src/CodeOfChaos.Extensions/DictionaryExtensions.cs @@ -0,0 +1,29 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- + +// ReSharper disable once CheckNamespace +namespace System; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +public static class DictionaryExtensions { + public static IDictionary AddOrUpdate(this IDictionary dictionary, TKey key, TValue value) where TKey : notnull { + if (dictionary.TryAdd(key, value)) return dictionary; + dictionary[key] = value; + return dictionary; + } + + public static bool TryAddToOrCreateCollection( + this IDictionary dictionary, + TKey key, + TValue value + ) where TCollection : ICollection, new() { + if (!dictionary.TryGetValue(key, out TCollection? collection)) return dictionary.TryAdd(key, [value]); + if (collection.Contains(value)) return false; + + collection.Add(value); + return true; + + } +} diff --git a/src/CodeOfChaos.Extensions/EnumExtensions.cs b/src/CodeOfChaos.Extensions/EnumExtensions.cs new file mode 100644 index 0000000..124c556 --- /dev/null +++ b/src/CodeOfChaos.Extensions/EnumExtensions.cs @@ -0,0 +1,52 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +// ReSharper disable once CheckNamespace +namespace System; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +public static class EnumExtensions { + private static readonly Dictionary EnumValuesCache = new(); + + /// + /// Retrieves all values of the specified enum type from the cache, falling back to reflection if uncached. + /// + private static IEnumerable GetEnumValues() where T : struct, Enum { + if (EnumValuesCache.TryGetValue(typeof(T), out var values)) return (T[])values; + + values = Enum.GetValues(); + EnumValuesCache[typeof(T)] = values; + return (T[])values; + } + + + /// + /// Retrieves all flagged values from the given Enum. + /// + /// The type of the Enum. + /// The enum value to inspect for flags. + /// Indicates whether the zero value (if present) should be excluded. + /// An enumerable containing the flagged values. + public static IEnumerable GetFlags(this T flagEnum, bool excludeZeroValue = true) where T : struct, Enum { + if (!typeof(T).IsDefined(typeof(FlagsAttribute), false)) + throw new ArgumentException("The provided enum type must have the [Flags] attribute.", nameof(flagEnum)); + + return GetEnumValues() + .Where(f => (!excludeZeroValue || !f.Equals(default(T))) && flagEnum.HasFlag(f)); + } + + /// + /// Retrieves all flagged values from the given Enum as an array. + /// + public static T[] GetFlagsAsArray(this T flagEnum, bool excludeZeroValue = true) where T : struct, Enum { + return GetFlags(flagEnum, excludeZeroValue).ToArray(); + } + + /// + /// Retrieves all flagged values from the given Enum as a list. + /// + public static List GetFlagsAsList(this T flagEnum, bool excludeZeroValue = true) where T : struct, Enum { + return GetFlags(flagEnum, excludeZeroValue).ToList(); + } +} diff --git a/src/CodeOfChaos.Extensions/LinqExtensions.cs b/src/CodeOfChaos.Extensions/LinqExtensions.cs index a96cddd..c0b5605 100644 --- a/src/CodeOfChaos.Extensions/LinqExtensions.cs +++ b/src/CodeOfChaos.Extensions/LinqExtensions.cs @@ -1,8 +1,10 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- +using System.Linq.Expressions; + // ReSharper disable once CheckNamespace -namespace System.Linq.Expressions; +namespace System; // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- diff --git a/src/CodeOfChaos.Extensions/README.md b/src/CodeOfChaos.Extensions/README.md new file mode 100644 index 0000000..13eca63 --- /dev/null +++ b/src/CodeOfChaos.Extensions/README.md @@ -0,0 +1,113 @@ +# CodeOfChaos.Extensions + +`CodeOfChaos.Extensions` is a collection of utility extension methods designed to enhance and simplify everyday programming tasks in .NET projects. This library offers extensions for tasks, strings, enums, LINQ queries, collections, and dictionaries, enabling more readable, efficient, and intuitive code. + +--- + +## Features + +### Task Extensions +Enhance task handling with support for **timeouts** and **cancellation tokens**: +- `.WithCancellation(CancellationToken)` – Cancel a task when the token is triggered. +- `.WithTimeout(TimeSpan)` – Enforce a timeout on task execution. +- Available for both `Task` and `Task`. + +--- + +### String Extensions +Simplify string management with useful utilities: +- `IsNullOrEmpty()` / `IsNotNullOrEmpty()` – Check for null or empty strings. +- `IsNullOrWhiteSpace()` / `IsNotNullOrWhiteSpace()` – Check for null, empty, or whitespace strings. +- `Truncate(int maxLength)` – Shorten a string to a maximum length. +- `ToGuid()` – Parse a string into a `Guid`. + +--- + +### Enum Extensions +Work with enums more efficiently: +- `.GetFlags()` – Retrieve all flagged values from an enum. +- `.GetFlagsAsArray()` / `.GetFlagsAsList()` – Retrieve flagged values as arrays or lists. +- Caches enums for better performance. + +--- + +### LINQ Extensions +Add conditional logic to LINQ operations: +- Apply operations conditionally: + - `ConditionalWhere` + - `ConditionalTake` + - `ConditionalOrderBy` + - `ConditionalDistinct` and more. +- Chain LINQ operations dynamically based on runtime conditions. + +--- + +### Collection Extensions +Extensions to handle collections and arrays more effectively: +- `IsEmpty()` – Check if a `string[]` or `IEnumerable` is empty. + +--- + +### Dictionary Extensions +Enhance dictionary management: +- `AddOrUpdate()` – Add a key-value pair or update the existing value. +- `TryAddToOrCreateCollection()` – Add values to a collection within a dictionary or create a new collection. + +--- + +## Installation + +This library targets `.NET 9.0` and requires C# 13.0. Ensure your project meets these requirements before using. + +Add the dependency to your project via NuGet: +```bash +dotnet add package CodeOfChaos.Extensions +``` + +--- + +## Usage + +Here’s how you can leverage the `CodeOfChaos.Extensions` library: + +### Task Example +```csharp +using System; +using System.Threading; +using System.Threading.Tasks; + +var tokenSource = new CancellationTokenSource(); +var task = Task.Delay(5000); // Example task +await task.WithCancellation(tokenSource.Token); +``` + +### String Example +```csharp +using System; + +string? input = "Hello, World!"; +if (input.IsNotNullOrWhiteSpace()) { + Console.WriteLine(input.Truncate(5)); // Output: Hello +} +``` + +### Enum Example +```csharp +[Flags] +enum AccessLevel { None = 0, Read = 1, Write = 2, Execute = 4 } + +var access = AccessLevel.Read | AccessLevel.Execute; +var flags = access.GetFlagsAsList(); // Output: [Read, Execute] +``` + +### LINQ Example +```csharp +var items = new List { 1, 2, 3, 4, 5 }.AsQueryable(); +var filtered = items.ConditionalWhere(true, x => x > 2); // Applies condition +``` + +--- + +## Contributing + +Feel free to fork and contribute to the project by submitting pull requests. When contributing, ensure your changes align with the project’s coding standards. diff --git a/src/CodeOfChaos.Extensions/StringExtensions.cs b/src/CodeOfChaos.Extensions/StringExtensions.cs new file mode 100644 index 0000000..cb9dc6e --- /dev/null +++ b/src/CodeOfChaos.Extensions/StringExtensions.cs @@ -0,0 +1,23 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +using System.Diagnostics.CodeAnalysis; + +// ReSharper disable once CheckNamespace +namespace System; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +public static class StringExtensions { + public static bool IsNullOrEmpty([NotNullWhen(false)] this string? str) => string.IsNullOrEmpty(str); + + public static bool IsNotNullOrEmpty([NotNullWhen(true)] this string? str) => !string.IsNullOrEmpty(str); + + public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? str) => string.IsNullOrWhiteSpace(str); + + public static bool IsNotNullOrWhiteSpace([NotNullWhen(true)] this string? str) => !string.IsNullOrWhiteSpace(str); + + public static string Truncate(this string input, int maxLength) => input.Length <= maxLength ? input : input[..maxLength]; + + public static Guid ToGuid(this string input) => Guid.Parse(input); +} diff --git a/src/CodeOfChaos.Extensions/TaskExtensions.cs b/src/CodeOfChaos.Extensions/TaskExtensions.cs new file mode 100644 index 0000000..2aefa34 --- /dev/null +++ b/src/CodeOfChaos.Extensions/TaskExtensions.cs @@ -0,0 +1,55 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +// ReSharper disable once CheckNamespace +namespace System; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +public static class TaskExtensions { + public static async Task WithCancellation(this Task task, CancellationToken cancellationToken) { + var tcs = new TaskCompletionSource(); + + await using (cancellationToken.Register(() => tcs.TrySetResult())) { + if (task != await Task.WhenAny(task, tcs.Task).ConfigureAwait(false)) + throw new OperationCanceledException(cancellationToken); + } + + await task.ConfigureAwait(false); + } + + public static async Task WithCancellation(this Task task, CancellationToken cancellationToken) { + var tcs = new TaskCompletionSource(); + + await using (cancellationToken.Register(() => tcs.TrySetResult())) { + if (task != await Task.WhenAny(task, tcs.Task).ConfigureAwait(false)) + throw new OperationCanceledException(cancellationToken); + } + + return await task.ConfigureAwait(false); + } + + public static async Task WithTimeout(this Task task, TimeSpan timeout) { + var tcs = new TaskCompletionSource(); + + using (var cts = new CancellationTokenSource(timeout)) + await using (cts.Token.Register(() => tcs.TrySetResult())) { + if (task != await Task.WhenAny(task, tcs.Task).ConfigureAwait(false)) + throw new TimeoutException("The operation has timed out."); + } + + await task.ConfigureAwait(false); + } + + public static async Task WithTimeout(this Task task, TimeSpan timeout) { + var tcs = new TaskCompletionSource(); + + using (var cts = new CancellationTokenSource(timeout)) + await using (cts.Token.Register(() => tcs.TrySetResult())) { + if (task != await Task.WhenAny(task, tcs.Task).ConfigureAwait(false)) + throw new TimeoutException("The operation has timed out."); + } + + return await task.ConfigureAwait(false); + } +} diff --git a/src/Tools.CodeOfChaos.Extensions/Program.cs b/src/Tools.CodeOfChaos.Extensions/Program.cs index 5f68f3a..b4cdb2c 100644 --- a/src/Tools.CodeOfChaos.Extensions/Program.cs +++ b/src/Tools.CodeOfChaos.Extensions/Program.cs @@ -1,13 +1,36 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- +using CodeOfChaos.CliArgsParser; +using CodeOfChaos.CliArgsParser.Library; + namespace Tools.CodeOfChaos.Extensions; // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- public static class Program { - public static async Task Main(string[] args) { + public async static Task Main(string[] args) { + // Register & Build the parser + // Don't forget to add the current assembly if you built more tools for the current project + CliArgsParser parser = CliArgsBuilder.CreateFromConfig( + config => { + config.AddCommandsFromAssemblyEntrypoint(); + } + ).Build(); + + // We are doing this here because else the launchSettings.json file becomes a humongous issue to deal with. + // Sometimes CLI params is not the answer. + // Code is the true saviour + string projects = string.Join(";", + "CodeOfChaos.Extensions", + "CodeOfChaos.Extensions.EntityFrameworkCore", + "CodeOfChaos.Extensions.AspNetCore", + "CodeOfChaos.Extensions.Serilog" + ); + string oneLineArgs = InputHelper.ToOneLine(args).Replace("%PROJECTS%", projects); + // Finally start executing + await parser.ParseAsync(oneLineArgs); } } diff --git a/src/Tools.CodeOfChaos.Extensions/Properties/launchSettings.json b/src/Tools.CodeOfChaos.Extensions/Properties/launchSettings.json new file mode 100644 index 0000000..57ed4cd --- /dev/null +++ b/src/Tools.CodeOfChaos.Extensions/Properties/launchSettings.json @@ -0,0 +1,24 @@ +{ + "profiles": { + "Major Version Bump": { + "commandName": "Project", + "commandLineArgs": "git-version-bump --section=\"major\" --projects=\"%PROJECTS%\" --push" + }, + "Minor Version Bump": { + "commandName": "Project", + "commandLineArgs": "git-version-bump --section=\"minor\" --projects=\"%PROJECTS%\" --push" + }, + "Patch Version Bump": { + "commandName": "Project", + "commandLineArgs": "git-version-bump --section=\"patch\" --projects=\"%PROJECTS%\" --push" + }, + "Preview Version Bump": { + "commandName": "Project", + "commandLineArgs": "git-version-bump --section=\"preview\" --projects=\"%PROJECTS%\" --push" + }, + "Download Icon": { + "commandName": "Project", + "commandLineArgs": "nuget-download-icon --origin=\"https://avatars.githubusercontent.com/u/102103683\" --projects=\"%PROJECTS%\" --root=\"\"" + } + } +} diff --git a/src/Tools.CodeOfChaos.Extensions/Tools.CodeOfChaos.Extensions.csproj b/src/Tools.CodeOfChaos.Extensions/Tools.CodeOfChaos.Extensions.csproj index f065911..fdba40c 100644 --- a/src/Tools.CodeOfChaos.Extensions/Tools.CodeOfChaos.Extensions.csproj +++ b/src/Tools.CodeOfChaos.Extensions/Tools.CodeOfChaos.Extensions.csproj @@ -8,4 +8,8 @@ enable + + + + diff --git a/tests/Tests.CodeOfChaos.Extensions.AspNetCore/LoggingOverrideExtensionTests.cs b/tests/Tests.CodeOfChaos.Extensions.AspNetCore/LoggingOverrideExtensionTests.cs new file mode 100644 index 0000000..9439cd0 --- /dev/null +++ b/tests/Tests.CodeOfChaos.Extensions.AspNetCore/LoggingOverrideExtensionTests.cs @@ -0,0 +1,135 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +using CodeOfChaos.Extensions.AspNetCore; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Events; +using ILogger=Serilog.ILogger; + +namespace Tests.CodeOfChaos.Extensions.AspNetCore; + +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +public class LoggingOverrideExtensionsTests { + + [Test] + public async Task OverrideLoggingWithSerilog_ShouldApplyDefaultSerilogConfiguration() { + // Arrange + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + + // Act + builder.OverrideLoggingWithSerilog(); + + // Assert + ServiceProvider serviceProvider = builder.Services.BuildServiceProvider(); + ILogger? logger = serviceProvider.GetService(); + await Assert.That(logger) + .IsNotNull() + .Because("ILogger should be registered in the service collection."); + await Assert.That(Log.Logger) + .IsNotNull() + .Because("Log.Logger should be initialized by Serilog with default configuration."); + } + + [Test] + public async Task OverrideLoggingWithSerilog_ShouldAllowCustomLoggerConfiguration() { + // Arrange + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + bool customConfigurationApplied = false; + + // Act + builder.OverrideLoggingWithSerilog(configure => { + customConfigurationApplied = true; + configure.MinimumLevel.Debug(); + }); + + // Assert + await Assert.That(customConfigurationApplied) + .IsTrue() + .Because("The custom configuration delegate should be applied when provided."); + } + + [Test] + public async Task OverrideLoggingWithSerilog_ShouldClearExistingLoggingProviders() { + // Arrange + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + builder.Logging.AddConsole(); // Add a console provider to verify it is cleared + + // Act + builder.OverrideLoggingWithSerilog(); + + // Assert + ServiceProvider serviceProvider = builder.Services.BuildServiceProvider(); + var loggerFactory = serviceProvider.GetService(); + await Assert.That(loggerFactory) + .IsNotNull() + .Because("The ILoggerFactory instance is required in the service collection."); + + // Count the logging providers + Microsoft.Extensions.Logging.ILogger? logger = loggerFactory?.CreateLogger("Test"); + await Assert.That(logger) + .IsNotNull() + .Because("Serilog should replace the existing providers when overriding logging."); + } + + [Test] + public async Task OverrideLoggingWithSerilog_ShouldRegisterApplicationShutdownCleanupService() { + // Arrange + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + + // Act + builder.OverrideLoggingWithSerilog(); + + // Assert + ServiceProvider serviceProvider = builder.Services.BuildServiceProvider(); + IEnumerable hostedServices = serviceProvider.GetServices().ToArray(); + await Assert.That(hostedServices).IsNotNull().Because("IHostedService collection should be available."); + await Assert.That(hostedServices.Any(service => service is LoggingOverrideExtensions.ApplicationShutdownLoggerCleanup)).IsTrue().Because("There should be at least one hosted service."); + } + + [Test] + public async Task ApplicationShutdownLoggerCleanup_ShouldFlushLogsOnStop() { + // Arrange + var cleanupService = new LoggingOverrideExtensions.ApplicationShutdownLoggerCleanup(); + bool flushCalled = false; + + // Replace the static Serilog Logger with a mock logger + ILogger originalLogger = Log.Logger; + Log.Logger = new LoggerConfiguration() + .WriteTo.Sink(new DelegatingSink(_ => flushCalled = true)) // Custom sink to detect flush calls + .CreateLogger(); + + + // Act + Log.Logger.Information("Test log message"); // Log a message to verify it is flushed + + await cleanupService.StartAsync(CancellationToken.None); + await cleanupService.StopAsync(CancellationToken.None); + + // Assert + await Assert.That(flushCalled) + .IsTrue() + .Because("Log.CloseAndFlush should be triggered when the application stops."); + + // Just to be sure + Log.Logger = originalLogger; // Restore the original logger + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Helper Classes +// --------------------------------------------------------------------------------------------------------------------- +public class DelegatingSink : Serilog.Core.ILogEventSink { + private readonly Action _write; + + public DelegatingSink(Action write) { + _write = write ?? throw new ArgumentNullException(nameof(write)); + } + + public void Emit(LogEvent logEvent) => _write.Invoke(logEvent); +} \ No newline at end of file diff --git a/tests/Tests.CodeOfChaos.Extensions.AspNetCore/Properties/launchSettings.json b/tests/Tests.CodeOfChaos.Extensions.AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..d2acbed --- /dev/null +++ b/tests/Tests.CodeOfChaos.Extensions.AspNetCore/Properties/launchSettings.json @@ -0,0 +1,7 @@ +{ + "profiles": { + "Test": { + "commandName": "Project" + } + } +} diff --git a/tests/Tests.CodeOfChaos.Extensions.AspNetCore/Tests.CodeOfChaos.Extensions.AspNetCore.csproj b/tests/Tests.CodeOfChaos.Extensions.AspNetCore/Tests.CodeOfChaos.Extensions.AspNetCore.csproj new file mode 100644 index 0000000..463c2cd --- /dev/null +++ b/tests/Tests.CodeOfChaos.Extensions.AspNetCore/Tests.CodeOfChaos.Extensions.AspNetCore.csproj @@ -0,0 +1,24 @@ + + + + net9.0 + latest + enable + enable + + false + true + + + + + + + + + + + + + + diff --git a/tests/Tests.CodeOfChaos.Extensions.EntityFrameworkCore/LinqExtensionTests.cs b/tests/Tests.CodeOfChaos.Extensions.EntityFrameworkCore/LinqExtensionTests.cs new file mode 100644 index 0000000..00600e9 --- /dev/null +++ b/tests/Tests.CodeOfChaos.Extensions.EntityFrameworkCore/LinqExtensionTests.cs @@ -0,0 +1,119 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; + +namespace Tests.CodeOfChaos.Extensions.EntityFrameworkCore; + +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +[TestSubject(typeof(LinqExtensions))] +public class LinqExtensionsTest { + + [Test] + [Arguments(true, new[] { "a", "b", "c" }, new[] { "a", "b", "c" })] + [Arguments(false, new[] { "a", "b", "c" }, new[] { "a", "b", "c" })] + public async Task ConditionalInclude_ShouldReturnSourceAsIs(bool condition, IEnumerable input, IEnumerable expected) { + // Arrange + IQueryable source = input.AsQueryable(); + + // Act + IQueryable output = source.ConditionalInclude(condition, x => x); + + // Assert + await Assert.That(output).IsEquivalentTo(expected); + } + + [Test] + [Arguments(true, "a", new[] { "a", "b", "a" }, new[] { "a", "a" })] + [Arguments(false, "a", new[] { "a", "b", "a" }, new[] { "a", "b", "a" })] + public async Task ConditionalWhere_ShouldFilterIfConditionIsTrue(bool condition, string filterValue, IEnumerable input, IEnumerable expected) { + // Arrange + IQueryable source = input.AsQueryable(); + + // Act + IQueryable output = source.ConditionalWhere(condition, x => x == filterValue); + + // Assert + await Assert.That(output).IsEquivalentTo(expected); + } + + [Test] + [Arguments(true, 2, new[] { 1, 2, 3, 4 }, new[] { 1, 2 })] + [Arguments(true, 3, new[] { 1, 2, 3, 4 }, new[] { 1, 2, 3 })] + [Arguments(true, 0, new[] { 1, 2, 3, 4 }, new int[] { })] + [Arguments(false, 2, new[] { 1, 2, 3, 4 }, new[] { 1, 2, 3, 4 })] + [Arguments(false, 3, new[] { 1, 2, 3, 4 }, new[] { 1, 2, 3, 4 })] + [Arguments(false, 0, new[] { 1, 2, 3, 4 }, new[] { 1, 2, 3, 4 })] + public async Task ConditionalTake_WithCount_ShouldReturnCorrectSubset(bool condition, int count, IEnumerable input, IEnumerable expected) { + // Arrange + IQueryable source = input.AsQueryable(); + + // Act + IQueryable output = source.ConditionalTake(condition, count); + + // Assert + await Assert.That(output).IsEquivalentTo(expected); + } + + [Test] + [Arguments(true, 1,3, new[] { 10, 20, 30, 40 }, new[] { 20, 30 })] + [Arguments(false, 2,5, new[] { 10, 20, 30, 40 }, new[] { 10, 20, 30, 40 })] + public async Task ConditionalTake_WithRange_ShouldReturnCorrectSubset(bool condition, int rangeStart, int rangeEnd, IEnumerable input, IEnumerable expected) { + // Arrange + IQueryable source = input.AsQueryable(); + + // Act + IQueryable output = source.ConditionalTake(condition, new Range(rangeStart, rangeEnd)); + + // Assert + await Assert.That(output).IsEquivalentTo(expected); + } + + [Test] + [Arguments(true, new[] { 5, 3, 8 }, new[] { 3, 5, 8 })] + [Arguments(false, new[] { 5, 3, 8 }, new[] { 5, 3, 8 })] + public async Task ConditionalOrderBy_ShouldOrderCorrectly(bool condition, IEnumerable input, IEnumerable expected) { + // Arrange + IQueryable source = input.AsQueryable(); + + // Act + IQueryable output = source.ConditionalOrderBy(condition, x => x); + + // Assert + await Assert.That(output).IsEquivalentTo(expected); + } + + [Test] + [Arguments(true, new[] { "b", "a", "c" }, null, new[] { "a", "b", "c" })] + [Arguments(false, new[] { "b", "a", "c" }, null, new[] { "b", "a", "c" })] + public async Task ConditionalOrderByWithComparer_ShouldOrderCorrectlyWithComparer(bool condition, IEnumerable input, IComparer? comparer, IEnumerable expected) { + // Arrange + IQueryable source = input.AsQueryable(); + comparer ??= StringComparer.Ordinal; + + // Act + IQueryable output = source.ConditionalOrderBy(condition, x => x, comparer); + + // Assert + await Assert.That(output).IsEquivalentTo(expected); + } + + [Test] + [Arguments(true, new[] { 5, 2, 8 }, new[] { 2, 5, 8 })] + [Arguments(false, new[] { 5, 2, 8 }, new[] { 5, 2, 8 })] + public async Task ConditionalOrderByNotNull_WithKeySelector_ShouldSortCorrectly(bool condition, IEnumerable input, IEnumerable expected) { + // Arrange + Expression>? orderBy = condition ? x => x : null; + IQueryable source = input.AsQueryable(); + + // Act + IQueryable output = source.ConditionalOrderByNotNull(orderBy); + + // Assert + await Assert.That(output).IsEquivalentTo(expected); + } +} \ No newline at end of file diff --git a/tests/Tests.CodeOfChaos.Extensions.EntityFrameworkCore/Properties/launchSettings.json b/tests/Tests.CodeOfChaos.Extensions.EntityFrameworkCore/Properties/launchSettings.json new file mode 100644 index 0000000..d2acbed --- /dev/null +++ b/tests/Tests.CodeOfChaos.Extensions.EntityFrameworkCore/Properties/launchSettings.json @@ -0,0 +1,7 @@ +{ + "profiles": { + "Test": { + "commandName": "Project" + } + } +} diff --git a/tests/Tests.CodeOfChaos.Extensions.EntityFrameworkCore/Tests.CodeOfChaos.Extensions.EntityFrameworkCore.csproj b/tests/Tests.CodeOfChaos.Extensions.EntityFrameworkCore/Tests.CodeOfChaos.Extensions.EntityFrameworkCore.csproj index c304e21..535181f 100644 --- a/tests/Tests.CodeOfChaos.Extensions.EntityFrameworkCore/Tests.CodeOfChaos.Extensions.EntityFrameworkCore.csproj +++ b/tests/Tests.CodeOfChaos.Extensions.EntityFrameworkCore/Tests.CodeOfChaos.Extensions.EntityFrameworkCore.csproj @@ -5,15 +5,20 @@ latest enable enable + false + true - - - - + + + + + + + diff --git a/tests/Tests.CodeOfChaos.Extensions.Serilog/LoggerExtensionTests.cs b/tests/Tests.CodeOfChaos.Extensions.Serilog/LoggerExtensionTests.cs new file mode 100644 index 0000000..fd0ae78 --- /dev/null +++ b/tests/Tests.CodeOfChaos.Extensions.Serilog/LoggerExtensionTests.cs @@ -0,0 +1,114 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +using CodeOfChaos.Extensions.Serilog; +using Moq; +using Serilog; + +namespace Tests.CodeOfChaos.Extensions.Serilog; + +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +public class LoggerExtensionsTests { + private Mock _mockLogger = new(); + + [Test] + public async Task ThrowableError_ShouldLogErrorAndThrowException() { + // Arrange + const string messageTemplate = "Error occurred: {ErrorDetail}"; + object[] propertyValues = ["An error detail"]; + _mockLogger.Setup(logger => logger.Error(It.IsAny(), messageTemplate, propertyValues)); + + // Act + Exception exception = _mockLogger.Object.ThrowableError(messageTemplate, propertyValues); + + // Assert + await Assert.That(exception).IsNotNull(); + await Assert.That(exception.Message).IsEqualTo(messageTemplate); + } + + [Test] + public async Task ThrowableError_TException_ShouldLogErrorAndThrowCustomException() { + // Arrange + const string messageTemplate = "Custom error occurred: {ErrorDetail}"; + object[] propertyValues = ["Custom exception property"]; + _mockLogger.Setup(logger => logger.Error(It.IsAny(), messageTemplate, propertyValues)); + + // Act + Exception exception = _mockLogger.Object.ThrowableError(messageTemplate, propertyValues); + + // Assert + await Assert.That(exception).IsNotNull() + .And.IsTypeOf(); + await Assert.That(exception.Message).IsEqualTo(messageTemplate); + } + + [Test] + public async Task ThrowableFatal_ShouldLogFatalAndThrowException() { + // Arrange + string messageTemplate = "Fatal error occurred: {ErrorDetail}"; + object[] propertyValues = { "Fatal error detail" }; + _mockLogger.Setup(logger => logger.Fatal(It.IsAny(), messageTemplate, propertyValues)); + + // Act + Exception exception = _mockLogger.Object.ThrowableFatal(messageTemplate, propertyValues); + + // Assert + await Assert.That(exception).IsNotNull(); + await Assert.That(exception.Message).IsEqualTo(messageTemplate); + } + + [Test] + public async Task ThrowableFatal_TException_ShouldLogFatalAndThrowCustomException() { + // Arrange + string messageTemplate = "Fatal custom error occurred: {ErrorDetail}"; + object[] propertyValues = { "Custom fatal detail" }; + _mockLogger.Setup(logger => logger.Fatal(It.IsAny(), messageTemplate, propertyValues)); + + // Act + Exception exception = _mockLogger.Object.ThrowableError(messageTemplate, propertyValues); + + // Assert + await Assert.That(exception).IsNotNull() + .And.IsTypeOf(); + await Assert.That(exception.Message).IsEqualTo(messageTemplate); + } + + [Test] + public async Task ThrowableFatal_WithExistingException_ShouldLogFatalAndUseProvidedException() { + // Arrange + string messageTemplate = "An existing exception occurred: {ErrorDetail}"; + var providedException = new InvalidOperationException("Existing exception"); + object[] propertyValues = { "Extra detail" }; + _mockLogger.Setup(logger => logger.Fatal(providedException, messageTemplate, propertyValues)); + + // Act + var exception = _mockLogger.Object.ThrowableFatal(providedException, messageTemplate, propertyValues); + + // Assert + await Assert.That(exception).IsNotNull(); + await Assert.That(exception.Message).IsEqualTo(providedException.Message); + await Assert.That(exception).IsEqualTo(providedException); + } + + [Test] + public async Task ExitFatal_ShouldLogFatalAndThrowExitException() { + // Arrange + int exitCode = 1; + string messageTemplate = "Critical failure - application exiting: {Reason}"; + object[] propertyValues = { "Unexpected failure" }; + + _mockLogger.Setup(logger => logger.Fatal(messageTemplate, propertyValues)); + + // Act + var exception = Assert.Throws(() => + _mockLogger.Object.ExitFatal(exitCode, messageTemplate, propertyValues) + ); + + // Assert + await Assert.That(exception).IsNotNull() + .And.IsTypeOf(); + await Assert.That(exception.Message).IsEqualTo(messageTemplate); + } +} \ No newline at end of file diff --git a/tests/Tests.CodeOfChaos.Extensions.Serilog/Properties/launchSettings.json b/tests/Tests.CodeOfChaos.Extensions.Serilog/Properties/launchSettings.json new file mode 100644 index 0000000..d2acbed --- /dev/null +++ b/tests/Tests.CodeOfChaos.Extensions.Serilog/Properties/launchSettings.json @@ -0,0 +1,7 @@ +{ + "profiles": { + "Test": { + "commandName": "Project" + } + } +} diff --git a/tests/Tests.CodeOfChaos.Extensions.Serilog/Tests.CodeOfChaos.Extensions.Serilog.csproj b/tests/Tests.CodeOfChaos.Extensions.Serilog/Tests.CodeOfChaos.Extensions.Serilog.csproj new file mode 100644 index 0000000..35df119 --- /dev/null +++ b/tests/Tests.CodeOfChaos.Extensions.Serilog/Tests.CodeOfChaos.Extensions.Serilog.csproj @@ -0,0 +1,24 @@ + + + + net9.0 + latest + enable + enable + + false + true + + + + + + + + + + + + + + diff --git a/tests/Tests.CodeOfChaos.Extensions/CollectionExtensionTests.cs b/tests/Tests.CodeOfChaos.Extensions/CollectionExtensionTests.cs new file mode 100644 index 0000000..edbbf2b --- /dev/null +++ b/tests/Tests.CodeOfChaos.Extensions/CollectionExtensionTests.cs @@ -0,0 +1,37 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +namespace Tests.CodeOfChaos.Extensions; + +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +public class CollectionExtensionTests { + [Test] + [Arguments(new string[] { }, true)] + [Arguments(new[] { "a" }, false)] + [Arguments(new[] { "a", "b" }, false)] + public async Task IsEmpty_Array_ShouldWork(string[] input, bool expected) { + // Arrange + + // Act + var output = input.IsEmpty(); + + // Assert + await Assert.That(output).IsEqualTo(expected); + } + + [Test] + [Arguments(new string[] { }, true)] + [Arguments(new[] { "a" }, false)] + [Arguments(new[] { "a", "b" }, false)] + public async Task IsEmpty_Enumerable_ShouldWork(IEnumerable input, bool expected) { + // Arrange + + // Act + bool output = input.IsEmpty(); + + // Assert + await Assert.That(output).IsEqualTo(expected); + } +} diff --git a/tests/Tests.CodeOfChaos.Extensions/DictionaryExtensionTests.cs b/tests/Tests.CodeOfChaos.Extensions/DictionaryExtensionTests.cs new file mode 100644 index 0000000..797f383 --- /dev/null +++ b/tests/Tests.CodeOfChaos.Extensions/DictionaryExtensionTests.cs @@ -0,0 +1,81 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +using JetBrains.Annotations; + +namespace Tests.CodeOfChaos.Extensions; + +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +[TestSubject(typeof(DictionaryExtensions))] +public class DictionaryExtensionsTest { + + [Test] + public async Task AddOrUpdate_ShouldAddNewKey_WhenKeyDoesNotExist() { + // Arrange + var dictionary = new Dictionary(); + + // Act + var result = dictionary.AddOrUpdate("key1", "value1"); + + // Assert + await Assert.That(result).IsEqualTo(dictionary); + await Assert.That(dictionary.ContainsKey("key1")).IsTrue(); + await Assert.That(dictionary["key1"]).IsEqualTo("value1"); + } + + [Test] + public async Task AddOrUpdate_ShouldUpdateValue_WhenKeyAlreadyExists() { + // Arrange + var dictionary = new Dictionary { { "key1", "value1" } }; + + // Act + var result = dictionary.AddOrUpdate("key1", "value2"); + + // Assert + await Assert.That(result).IsEqualTo(dictionary); + await Assert.That(dictionary.ContainsKey("key1")).IsTrue(); + await Assert.That(dictionary["key1"]).IsEqualTo("value2"); + } + + [Test] + public async Task TryAddToOrCreateCollection_ShouldAddValueToNewCollection_WhenKeyDoesNotExist() { + // Arrange + var dictionary = new Dictionary>(); + + // Act + var result = dictionary.TryAddToOrCreateCollection("key1", 1); + + // Assert + await Assert.That(result).IsTrue(); + await Assert.That(dictionary.ContainsKey("key1")).IsTrue(); + await Assert.That(dictionary["key1"]).IsEquivalentTo(new List { 1 }); + } + + [Test] + public async Task TryAddToOrCreateCollection_ShouldAddValueToExistingCollection_WhenKeyExistsAndValueIsNew() { + // Arrange + var dictionary = new Dictionary> { { "key1", [1] } }; + + // Act + var result = dictionary.TryAddToOrCreateCollection("key1", 2); + + // Assert + await Assert.That(result).IsTrue(); + await Assert.That(dictionary["key1"]).IsEquivalentTo(new List { 1, 2 }); + } + + [Test] + public async Task TryAddToOrCreateCollection_ShouldNotAddValue_WhenValueAlreadyExistsInCollection() { + // Arrange + var dictionary = new Dictionary> { { "key1", [1] } }; + + // Act + var result = dictionary.TryAddToOrCreateCollection("key1", 1); + + // Assert + await Assert.That(result).IsFalse(); + await Assert.That(dictionary["key1"]).IsEquivalentTo(new List { 1 }); + } +} \ No newline at end of file diff --git a/tests/Tests.CodeOfChaos.Extensions/EnumExtensionTests.cs b/tests/Tests.CodeOfChaos.Extensions/EnumExtensionTests.cs new file mode 100644 index 0000000..990bd5a --- /dev/null +++ b/tests/Tests.CodeOfChaos.Extensions/EnumExtensionTests.cs @@ -0,0 +1,142 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +using JetBrains.Annotations; + +namespace Tests.CodeOfChaos.Extensions; + +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +[TestSubject(typeof(EnumExtensions))] +public class EnumExtensionsTest { + + [Flags] + private enum TestFlags { + None = 0, + Flag1 = 1 << 0, // 1 + Flag2 = 1 << 1, // 2 + Flag3 = 1 << 2, // 4 + Flag4 = 1 << 3 // 8 + } + + // ReSharper disable UnusedMember.Local + private enum NonFlagsEnum { + Value1 = 0, + Value2 = 1, + Value3 = 2 + } + + [Test] + public async Task GetFlags_ShouldReturnCorrectFlags_WhenExcludeZeroValueIsTrue() { + // Arrange + const TestFlags enumValue = TestFlags.Flag1 | TestFlags.Flag3; + + // Act + IEnumerable result = enumValue.GetFlags(); + + // Assert + var expected = new[] { TestFlags.Flag1, TestFlags.Flag3 }; + await Assert.That(result).IsEquivalentTo(expected); + } + + [Test] + public async Task GetFlags_ShouldReturnCorrectFlagsIncludingZero_WhenExcludeZeroValueIsFalse() { + // Arrange + const TestFlags enumValue = TestFlags.None | TestFlags.Flag2; + + // Act + IEnumerable result = enumValue.GetFlags(false); + + // Assert + var expected = new[] { TestFlags.None, TestFlags.Flag2 }; + await Assert.That(result).IsEquivalentTo(expected); + } + + [Test] + public async Task GetFlags_ShouldReturnAllFlags_WhenAllFlagsAreSet() { + // Arrange + const TestFlags enumValue = TestFlags.Flag1 | TestFlags.Flag2 | TestFlags.Flag3 | TestFlags.Flag4; + + // Act + IEnumerable result = enumValue.GetFlags(); + + // Assert + var expected = new[] { TestFlags.Flag1, TestFlags.Flag2, TestFlags.Flag3, TestFlags.Flag4 }; + await Assert.That(result).IsEquivalentTo(expected); + } + + [Test] + public async Task GetFlags_ShouldReturnEmpty_WhenNoFlagIsSetAndZeroIsExcluded() { + // Arrange + const TestFlags enumValue = TestFlags.None; + + // Act + IEnumerable result = enumValue.GetFlags(); + + // Assert + TestFlags[] expected = Array.Empty(); + await Assert.That(result).IsEquivalentTo(expected); + } + + [Test] + public async Task GetFlags_ShouldThrowArgumentException_WhenEnumTypeIsNotFlags() { + // Arrange + const NonFlagsEnum nonFlagsEnumValue = NonFlagsEnum.Value2; + + // Act & Assert + await Assert.ThrowsAsync(() => Task.Run(() => nonFlagsEnumValue.GetFlags())); + } + + [Test] + public async Task GetFlagsAsArray_ShouldReturnArrayOfFlags_WhenCalled() { + // Arrange + const TestFlags enumValue = TestFlags.Flag2 | TestFlags.Flag4; + + // Act + TestFlags[] result = enumValue.GetFlagsAsArray(); + + // Assert + var expected = new[] { TestFlags.Flag2, TestFlags.Flag4 }; + await Assert.That(result).IsEquivalentTo(expected); + } + + [Test] + public async Task GetFlagsAsList_ShouldReturnListOfFlags_WhenCalled() { + // Arrange + const TestFlags enumValue = TestFlags.Flag1 | TestFlags.Flag3; + + // Act + List result = enumValue.GetFlagsAsList(); + + // Assert + var expected = new List { TestFlags.Flag1, TestFlags.Flag3 }; + await Assert.That(result).IsEquivalentTo(expected); + } + + [Test] + public async Task GetFlagsAsArray_ShouldIncludeZero_WhenExcludeZeroValueIsFalse() { + // Arrange + const TestFlags enumValue = TestFlags.Flag3 | TestFlags.None; + + // Act + TestFlags[] result = enumValue.GetFlagsAsArray(false); + + // Assert + var expected = new[] { TestFlags.None, TestFlags.Flag3 }; + await Assert.That(result).IsEquivalentTo(expected); + } + + [Test] + public async Task GetFlagsAsList_ShouldExcludeZero_WhenExcludeZeroValueIsTrue() { + // Arrange + TestFlags enumValue = TestFlags.Flag1 | TestFlags.None; + + // Act + List result = enumValue.GetFlagsAsList(); + + // Assert + var expected = new List { TestFlags.Flag1 }; + await Assert.That(result).IsEquivalentTo(expected); + } +} \ No newline at end of file diff --git a/tests/Tests.CodeOfChaos.Extensions/LinqExtensionTests.cs b/tests/Tests.CodeOfChaos.Extensions/LinqExtensionTests.cs index 9593a94..f927c2b 100644 --- a/tests/Tests.CodeOfChaos.Extensions/LinqExtensionTests.cs +++ b/tests/Tests.CodeOfChaos.Extensions/LinqExtensionTests.cs @@ -1,17 +1,13 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- -using JetBrains.Annotations; using System.Linq.Expressions; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; -using TUnit.Core; -namespace Tests.CodeOfChaos.Extensions; +// ReSharper disable once CheckNamespace +namespace System.Linq; // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- -[TestSubject(typeof(LinqExtensions))] public class LinqExtensionsTests { private readonly List _data = Enumerable.Range(1, 10).ToList();// Data: {1, 2, 3, ... 10} @@ -20,7 +16,7 @@ public async Task ConditionalWhere_ConditionTrue_ShouldFilter() { IQueryable source = _data.AsQueryable(); List result = source.ConditionalWhere(true, predicate: x => x > 5).ToList(); - await Assert.That(result).IsEqualTo([ + await Assert.That(result).IsEquivalentTo([ 6, 7, 8, 9, 10 ]); } @@ -30,7 +26,7 @@ public async Task ConditionalWhere_ConditionFalse_ShouldNotFilter() { IQueryable source = _data.AsQueryable(); List result = source.ConditionalWhere(false, predicate: x => x > 5).ToList(); - await Assert.That(result).IsEqualTo(_data); + await Assert.That(result).IsEquivalentTo(_data); } [Test] @@ -38,7 +34,7 @@ public async Task ConditionalTake_ConditionTrue_ShouldTakeElements() { IQueryable source = _data.AsQueryable(); List result = source.ConditionalTake(true, 5).ToList(); - await Assert.That(result).IsEqualTo([ + await Assert.That(result).IsEquivalentTo([ 1, 2, 3, 4, 5 ]); } @@ -48,7 +44,7 @@ public async Task ConditionalTake_ConditionFalse_ShouldNotTakeElements() { IQueryable source = _data.AsQueryable(); List result = source.ConditionalTake(false, 5).ToList(); - await Assert.That(result).IsEqualTo(_data); + await Assert.That(result).IsEquivalentTo(_data); } [Test] @@ -56,7 +52,7 @@ public async Task ConditionalOrderBy_ConditionTrue_ShouldOrder() { IQueryable source = _data.AsQueryable(); List result = source.ConditionalOrderBy(true, orderBy: x => -x).ToList(); - await Assert.That(result).IsEqualTo([ + await Assert.That(result).IsEquivalentTo([ 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 ]); } @@ -66,7 +62,7 @@ public async Task ConditionalOrderBy_ConditionFalse_ShouldNotOrder() { IQueryable source = _data.AsQueryable(); List result = source.ConditionalOrderBy(false, orderBy: x => -x).ToList(); - await Assert.That(result).IsEqualTo(_data); + await Assert.That(result).IsEquivalentTo(_data); } [Test] @@ -74,7 +70,7 @@ public async Task ConditionalSkip_ConditionTrue_ShouldSkipElements() { IQueryable source = _data.AsQueryable(); List result = source.ConditionalSkip(true, 5).ToList(); - await Assert.That(result).IsEqualTo([ + await Assert.That(result).IsEquivalentTo([ 6, 7, 8, 9, 10 ]); } @@ -84,7 +80,7 @@ public async Task ConditionalSkip_ConditionFalse_ShouldNotSkipElements() { IQueryable source = _data.AsQueryable(); List result = source.ConditionalSkip(false, 5).ToList(); - await Assert.That(result).IsEqualTo(_data); + await Assert.That(result).IsEquivalentTo(_data); } [Test] @@ -92,7 +88,7 @@ public async Task ConditionalDistinct_ConditionTrue_ShouldReturnDistinct() { IQueryable source = new List { 1, 2, 2, 3, 3, 3 }.AsQueryable(); List result = source.ConditionalDistinct(true).ToList(); - await Assert.That(result).IsEqualTo([ + await Assert.That(result).IsEquivalentTo([ 1, 2, 3 ]); } @@ -102,7 +98,7 @@ public async Task ConditionalDistinct_ConditionFalse_ShouldReturnOriginal() { IQueryable source = new List { 1, 2, 2, 3, 3, 3 }.AsQueryable(); List result = source.ConditionalDistinct(false).ToList(); - await Assert.That(result).IsEqualTo([ + await Assert.That(result).IsEquivalentTo([ 1, 2, 2, 3, 3, 3 ]); } @@ -113,7 +109,7 @@ public async Task ConditionalUnion_ConditionTrue_ShouldUnion() { IQueryable second = new List { 3, 4, 5 }.AsQueryable(); List result = source.ConditionalUnion(true, second).ToList(); - await Assert.That(result).IsEqualTo([ + await Assert.That(result).IsEquivalentTo([ 1, 2, 3, 4, 5 ]); } @@ -124,7 +120,7 @@ public async Task ConditionalUnion_ConditionFalse_ShouldNotUnion() { IQueryable second = new List { 3, 4, 5 }.AsQueryable(); List result = source.ConditionalUnion(false, second).ToList(); - await Assert.That(result).IsEqualTo([ + await Assert.That(result).IsEquivalentTo([ 1, 2, 3 ]); } @@ -135,7 +131,7 @@ public async Task ConditionalExcept_ConditionTrue_ShouldApplyExcept() { IQueryable second = new List { 3, 4, 5 }.AsQueryable(); List result = source.ConditionalExcept(true, second).ToList(); - await Assert.That(result).IsEqualTo([ + await Assert.That(result).IsEquivalentTo([ 1, 2 ]); } @@ -146,7 +142,7 @@ public async Task ConditionalExcept_ConditionFalse_ShouldNotApplyExcept() { IQueryable second = new List { 3, 4, 5 }.AsQueryable(); List result = source.ConditionalExcept(false, second).ToList(); - await Assert.That(result).IsEqualTo([ + await Assert.That(result).IsEquivalentTo([ 1, 2, 3, 4 ]); } diff --git a/tests/Tests.CodeOfChaos.Extensions/Properties/launchSettings.json b/tests/Tests.CodeOfChaos.Extensions/Properties/launchSettings.json new file mode 100644 index 0000000..d2acbed --- /dev/null +++ b/tests/Tests.CodeOfChaos.Extensions/Properties/launchSettings.json @@ -0,0 +1,7 @@ +{ + "profiles": { + "Test": { + "commandName": "Project" + } + } +} diff --git a/tests/Tests.CodeOfChaos.Extensions/StringExtensionTests.cs b/tests/Tests.CodeOfChaos.Extensions/StringExtensionTests.cs new file mode 100644 index 0000000..4cd6a24 --- /dev/null +++ b/tests/Tests.CodeOfChaos.Extensions/StringExtensionTests.cs @@ -0,0 +1,113 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +namespace Tests.CodeOfChaos.Extensions; + +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +public class StringExtensionTests { + [Test] + [Arguments(null, true)] + [Arguments("", true)] + [Arguments(" ", false)] + [Arguments("a", false)] + public async Task IsNullOrEmpty_ShouldWork(string? input, bool expected) { + // Arrange + + // Act + var output = input.IsNullOrEmpty(); + + // Assert + await Assert.That(output).IsEqualTo(expected); + } + + [Test] + [Arguments(null, false)] + [Arguments("", false)] + [Arguments(" ", true)] + [Arguments("a", true)] + public async Task IsNotNullOrEmpty_ShouldWork(string? input, bool expected) { + // Arrange + + // Act + var output = input.IsNotNullOrEmpty(); + + // Assert + await Assert.That(output).IsEqualTo(expected); + } + + [Test] + [Arguments(null, true)] + [Arguments("", true)] + [Arguments(" ", true)] + [Arguments("a", false)] + public async Task IsNullOrWhitespace_ShouldWork(string? input, bool expected) { + // Arrange + + // Act + var output = input.IsNullOrWhiteSpace(); + + // Assert + await Assert.That(output).IsEqualTo(expected); + } + + [Test] + [Arguments(null, false)] + [Arguments("", false)] + [Arguments(" ", false)] + [Arguments("a", true)] + public async Task IsNotNullOrWhitespace_ShouldWork(string? input, bool expected) { + // Arrange + + // Act + bool output = input.IsNotNullOrWhiteSpace(); + + // Assert + await Assert.That(output).IsEqualTo(expected); + } + + [Test] + [Arguments("test", 10, "test")] + [Arguments("test", 4, "test")] + [Arguments("testing", 4, "test")] + [Arguments("hello world", 5, "hello")] + public async Task Truncate_ShouldWork(string input, int maxLength, string expected) { + // Arrange + + // Act + string output = input.Truncate(maxLength); + + // Assert + await Assert.That(output).IsEqualTo(expected); + } + + [Test] + [Arguments("d3cd4cfa-1cdf-4711-8d41-7e33a8d749fb", "d3cd4cfa-1cdf-4711-8d41-7e33a8d749fb")] + [Arguments("00000000-0000-0000-0000-000000000000", "00000000-0000-0000-0000-000000000000")] + public async Task ToGuid_ShouldReturnGuid_WhenInputIsValid(string input, string expectedGuidString) { + // Arrange + Guid expected = Guid.Parse(expectedGuidString); + + // Act + var output = input.ToGuid(); + + // Assert + await Assert.That(output).IsEqualTo(expected); + } + + [Test] + [Arguments("")] + [Arguments(" ")] + [Arguments("InvalidGuidFormat")] + [Arguments("1234")] + public async Task ToGuid_ShouldThrowException_WhenInputIsInvalid(string input) { + // Arrange + + // Act + Func act = input.ToGuid; + + // Assert + await Assert.That(act).Throws(); + } +} diff --git a/tests/Tests.CodeOfChaos.Extensions/TaskExtensionTests.cs b/tests/Tests.CodeOfChaos.Extensions/TaskExtensionTests.cs new file mode 100644 index 0000000..46a6722 --- /dev/null +++ b/tests/Tests.CodeOfChaos.Extensions/TaskExtensionTests.cs @@ -0,0 +1,144 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +namespace Tests.CodeOfChaos.Extensions; + +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +public class TaskExtensionsTest { + + [Test] + public async Task WithCancellation_ShouldComplete_WhenTokenIsNotCanceled() { + // Arrange + var tokenSource = new CancellationTokenSource(); + Task task = DummyTask(); + + // Act + await task.WithCancellation(tokenSource.Token); + + // Assert + await Assert.That(task.IsCompleted).IsTrue(); // Validate the task is completed + } + + [Test] + public async Task WithCancellation_ShouldThrowOperationCanceledException_WhenTokenIsCanceled() { + // Arrange + var tokenSource = new CancellationTokenSource(); + Task task = Task.Delay(1000, tokenSource.Token); + + // Act + await tokenSource.CancelAsync(); // Cancel token immediately + + // Assert + await Assert.ThrowsAsync(() => task.WithCancellation(tokenSource.Token)); + } + + [Test] + public async Task WithCancellation_Generic_ShouldComplete_WhenTokenIsNotCanceled() { + // Arrange + var tokenSource = new CancellationTokenSource(); + Task task = DummyTaskWithResult(); + + // Act + string result = await task.WithCancellation(tokenSource.Token); + + // Assert + await Assert.That(task.IsCompleted).IsTrue(); + await Assert.That(result).IsEqualTo("Completed"); + } + + [Test] + public async Task WithCancellation_Generic_ShouldThrowOperationCanceledException_WhenTokenIsCanceled() { + // Arrange + var tokenSource = new CancellationTokenSource(); + Task task = Task.Delay(1000, tokenSource.Token).ContinueWith(_ => "This will not complete", tokenSource.Token); + + // Act + await tokenSource.CancelAsync(); + + // Assert + await Assert.ThrowsAsync(() => task.WithCancellation(tokenSource.Token)); + } + + [Test] + public async Task WithTimeout_ShouldComplete_WhenTaskFinishesBeforeTimeout() { + // Arrange + Task task = DummyTask(); + + // Act + await task.WithTimeout(TimeSpan.FromSeconds(1)); + + // Assert + await Assert.That(task.IsCompleted).IsTrue(); // Validate the task is completed + } + + [Test] + public async Task WithTimeout_ShouldThrowTimeoutException_WhenTaskExceedsTimeout() { + // Arrange + Task task = Task.Delay(2000); // Simulate a long-running task + + // Assert + await Assert.ThrowsAsync(() => task.WithTimeout(TimeSpan.FromMilliseconds(500))); + } + + [Test] + public async Task WithTimeout_Generic_ShouldComplete_WhenTaskFinishesBeforeTimeout() { + // Arrange + Task task = DummyTaskWithResult(); + + // Act + string result = await task.WithTimeout(TimeSpan.FromSeconds(2)); + + // Assert + await Assert.That(task.IsCompleted).IsTrue(); + await Assert.That(result).IsEqualTo("Completed"); + } + + [Test] + public async Task WithTimeout_Generic_ShouldThrowTimeoutException_WhenTaskExceedsTimeout() { + // Arrange + Task task = Task.Delay(2000).ContinueWith(_ => "This will timeout"); + + // Assert + await Assert.ThrowsAsync(() => task.WithTimeout(TimeSpan.FromMilliseconds(500))); + } + + [Test] + public async Task WithTimeout_AndCancellation_ShouldPrioritizeCancellationOverTimeout() { + // Arrange + var tokenSource = new CancellationTokenSource(); + Task task = Task.Delay(2000, tokenSource.Token); // Simulate a long-running task + + // Act + await tokenSource.CancelAsync(); // Cancel the task before timeout + + // Assert + await Assert.ThrowsAsync(() => task.WithCancellation(tokenSource.Token).WithTimeout(TimeSpan.FromSeconds(5))); + } + + [Test] + public async Task WithTimeout_AndCancellation_ShouldCancelGenericTask() { + // Arrange + var tokenSource = new CancellationTokenSource(); + Task task = Task.Delay(2000, tokenSource.Token).ContinueWith(_ => "This will be canceled", tokenSource.Token); + + // Act + await tokenSource.CancelAsync(); + + // Assert + await Assert.ThrowsAsync(() => task.WithCancellation(tokenSource.Token).WithTimeout(TimeSpan.FromSeconds(5))); + } + + // ----------------------------------------------------------------------------------------------------------------- + // Helpers + // ----------------------------------------------------------------------------------------------------------------- + private async Task DummyTask() { + await Task.Delay(100); // Simulate a short operation + } + + private async Task DummyTaskWithResult() { + await Task.Delay(100); // Simulate a short operation + return "Completed"; + } +} \ No newline at end of file diff --git a/tests/Tests.CodeOfChaos.Extensions/Tests.CodeOfChaos.Extensions.csproj b/tests/Tests.CodeOfChaos.Extensions/Tests.CodeOfChaos.Extensions.csproj index 91705e3..5f3d632 100644 --- a/tests/Tests.CodeOfChaos.Extensions/Tests.CodeOfChaos.Extensions.csproj +++ b/tests/Tests.CodeOfChaos.Extensions/Tests.CodeOfChaos.Extensions.csproj @@ -5,16 +5,16 @@ latest enable enable + false true - - - - + + +