From c02ab7d0992cb504b959b6574a36ef32e90f6889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=9F=D0=B8=D1=81?= =?UTF-8?q?=D1=8C=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9?= Date: Wed, 6 Nov 2024 16:29:58 +0300 Subject: [PATCH 1/3] Add net8. Drop net5 and net6. - Namespace per file. - Drop DataTables support.. --- .editorconfig | 295 ++++++ .github/workflows/netcorelibrary.yml | 2 +- .../HelperExtensionsTests.cs | 95 +- .../Monq.Core.Paging.Tests.csproj | 12 +- .../MultiLevelPagingSortingTests.cs | 491 +++++----- .../PagingExtensionsTests.cs | 843 ++++++------------ .../PagingHelperTests.cs | 137 ++- .../PagingResponseHeadersTests.cs | 69 +- src/Monq.Core.Paging.Tests/TestHelpers.cs | 59 +- src/Monq.Core.Paging/Constants.cs | 21 +- .../Extensions/ExpressionHelperExtensions.cs | 501 ++++++----- .../Extensions/HttpExtensions.cs | 121 ++- .../Extensions/PagingExtensions.cs | 566 +++++------- .../Extensions/PagingUriExtensions.cs | 133 +-- .../Extensions/QueryableExtensions.cs | 217 +++-- .../Helpers/ExpressionConstantCallVisitor.cs | 31 +- src/Monq.Core.Paging/Helpers/PagingHelper.cs | 107 ++- .../Models/PagingResponseHeaders.cs | 75 +- src/Monq.Core.Paging/Models/Searching.cs | 111 ++- src/Monq.Core.Paging/Monq.Core.Paging.csproj | 31 +- 20 files changed, 1829 insertions(+), 2088 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2fa928a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,295 @@ +# Удалите строку ниже, если вы хотите наследовать параметры .editorconfig из каталогов, расположенных выше в иерархии +root = true + +# Все файлы +[*] +charset = utf-8 + +# Файлы Protobuf +[*.proto] +indent_size = 2 +indent_style = space + +# Файлы csproj +[*.csproj] +tab_size = 2 +indent_size = 2 +indent_style = space + +# Файлы C# +[*.cs] + +#### Основные параметры EditorConfig #### + +# Отступы и интервалы +indent_size = 4 +indent_style = space +tab_width = 4 + +# Предпочтения для новых строк +end_of_line = crlf +insert_final_newline = true + +#### Параметры MONQ +csharp_default_private_modifier = implicit + +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = error +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +##### Namespace per file scope +csharp_style_namespace_declarations = file_scoped:suggestion +csharp_indent_braces = false + +spelling_languages = en-us,ru-ru + +#### Рекомендации по написанию кода .NET #### + +# Упорядочение Using +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false + +# Предпочтения для this. и Me. +dotnet_style_qualification_for_event = false:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_property = false:silent + +# Параметры использования ключевых слов языка и типов BCL +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent + +# Предпочтения для скобок +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent + +# Предпочтения модификатора +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent + +# Выражения уровень предпочтения +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_throw_expression = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion + +# Предпочтения для полей +dotnet_style_readonly_field = true:suggestion + +# Настройки параметров +dotnet_code_quality_unused_parameters = all:suggestion + +#### Рекомендации по написанию кода C# #### + +# Предпочтения var +csharp_style_var_elsewhere = true:suggestion +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion + +# Члены, заданные выражениями +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:suggestion +csharp_style_expression_bodied_local_functions = false:suggestion +csharp_style_expression_bodied_methods = true:suggestion +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:suggestion + +# Настройки соответствия шаблонов +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion + +# Настройки проверки на null +csharp_style_conditional_delegate_call = true:suggestion + +# Предпочтения модификатора +csharp_prefer_static_local_function = true:suggestion +csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async + +# Предпочтения для блоков кода +csharp_prefer_braces = false:silent +csharp_prefer_simple_using_statement = false:suggestion + +# Выражения уровень предпочтения +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + +# предпочтения для директивы using +csharp_using_directive_placement = outside_namespace:silent + +#### Правила форматирования C# #### + +# Предпочтения для новых строк +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Предпочтения для отступов +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Предпочтения для интервалов +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Предпочтения переноса +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Стили именования #### + +# Правила именования + +dotnet_naming_rule.interface_should_be_начинается_с_i.severity = suggestion +dotnet_naming_rule.interface_should_be_начинается_с_i.symbols = interface +dotnet_naming_rule.interface_should_be_начинается_с_i.style = начинается_с_i + +dotnet_naming_rule.типы_should_be_всечастиспрописнойбуквы.severity = suggestion +dotnet_naming_rule.типы_should_be_всечастиспрописнойбуквы.symbols = типы +dotnet_naming_rule.типы_should_be_всечастиспрописнойбуквы.style = pascal_case_style + +dotnet_naming_rule.не_являющиеся_полем_члены_should_be_всечастиспрописнойбуквы.severity = suggestion +dotnet_naming_rule.не_являющиеся_полем_члены_should_be_всечастиспрописнойбуквы.symbols = не_являющиеся_полем_члены +dotnet_naming_rule.не_являющиеся_полем_члены_should_be_всечастиспрописнойбуквы.style = pascal_case_style + +dotnet_naming_rule.частное_или_внутреннее_поле_should_be_начинается_с__.severity = error +dotnet_naming_rule.частное_или_внутреннее_поле_should_be_начинается_с__.symbols = частное_или_внутреннее_поле +dotnet_naming_rule.частное_или_внутреннее_поле_should_be_начинается_с__.style = начинается_с__ + +# Спецификации символов + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.частное_или_внутреннее_поле.applicable_kinds = field +dotnet_naming_symbols.частное_или_внутреннее_поле.applicable_accessibilities = internal, private +dotnet_naming_symbols.частное_или_внутреннее_поле.required_modifiers = + +dotnet_naming_symbols.типы.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.типы.applicable_accessibilities = public, internal, private, protected, protected_internal +dotnet_naming_symbols.типы.required_modifiers = + +dotnet_naming_symbols.не_являющиеся_полем_члены.applicable_kinds = property, event, method +dotnet_naming_symbols.не_являющиеся_полем_члены.applicable_accessibilities = public, internal, private, protected, protected_internal +dotnet_naming_symbols.не_являющиеся_полем_члены.required_modifiers = + +# Стили именования + +dotnet_naming_style.всечастиспрописнойбуквы.required_prefix = +dotnet_naming_style.всечастиспрописнойбуквы.required_suffix = +dotnet_naming_style.всечастиспрописнойбуквы.word_separator = +dotnet_naming_style.всечастиспрописнойбуквы.capitalization = pascal_case + +dotnet_naming_style.начинается_с_i.required_prefix = I +dotnet_naming_style.начинается_с_i.required_suffix = +dotnet_naming_style.начинается_с_i.word_separator = +dotnet_naming_style.начинается_с_i.capitalization = pascal_case + +dotnet_naming_style.начинается_с__.required_prefix = _ +dotnet_naming_style.начинается_с__.required_suffix = +dotnet_naming_style.начинается_с__.word_separator = +dotnet_naming_style.начинается_с__.capitalization = camel_case +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_pattern_matching = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent +csharp_style_prefer_readonly_struct = true:suggestion +csharp_style_prefer_readonly_struct_member = true:suggestion + +[*.{cs,vb}] +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion +end_of_line = crlf +dotnet_code_quality_unused_parameters = all:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning +dotnet_style_allow_multiple_blank_lines_experimental = false:warning +dotnet_style_allow_statement_immediately_after_block_experimental = true:silent +dotnet_style_readonly_field = true:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_require_accessibility_modifiers = omit_if_default:suggestion \ No newline at end of file diff --git a/.github/workflows/netcorelibrary.yml b/.github/workflows/netcorelibrary.yml index f38e73b..999231b 100644 --- a/.github/workflows/netcorelibrary.yml +++ b/.github/workflows/netcorelibrary.yml @@ -27,7 +27,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - name: Restore dependencies run: dotnet restore - name: Build diff --git a/src/Monq.Core.Paging.Tests/HelperExtensionsTests.cs b/src/Monq.Core.Paging.Tests/HelperExtensionsTests.cs index 819f77c..4045511 100644 --- a/src/Monq.Core.Paging.Tests/HelperExtensionsTests.cs +++ b/src/Monq.Core.Paging.Tests/HelperExtensionsTests.cs @@ -1,82 +1,47 @@ -using Monq.Core.Paging.Extensions; +using Monq.Core.Paging.Extensions; using Monq.Core.Paging.Models; -using Monq.Core.Paging.Models.DataTable; using Xunit; -namespace Monq.Core.Paging.Tests +namespace Monq.Core.Paging.Tests; + +public class HelperExtensionsTests { - public class HelperExtensionsTests + [Fact(DisplayName = "Проверка экранирования \"+\" в uri.")] + public void ShouldEscapePlusSymbolInSearch() { - [Fact(DisplayName = "Проверка экранирования \"+\" в uri.")] - public void ShouldEscapePlusSymbolInSearch() - { - const string defaultUrl = "http://localhost:5005/api/systems/filter"; - const string expectedUrl = "http://localhost:5005/api/systems/filter?&page=1&perPage=1&search=%2B5"; - - var pagingModel = new PagingModel { PerPage = 1, Search = "+5" }; - - var url = pagingModel.GetUri(defaultUrl); - - Assert.Equal(expectedUrl, url); - } - - [Fact(DisplayName = "Проверка возвращения Url без ? от PagingModel.")] - public void ShouldReturnUrlWithoutQuestionMarkFromPagingModel() - { - // TODO: Выяснить почему тут испольузется ?& - const string defaultUrl = "http://localhost:5005/api/systems/filter"; - const string expectedUrl = "http://localhost:5005/api/systems/filter?&page=1&perPage=2&search=1"; - var pagingModel = new PagingModel { PerPage = 2, Search = "1" }; + const string defaultUrl = "http://localhost:5005/api/systems/filter"; + const string expectedUrl = "http://localhost:5005/api/systems/filter?&page=1&perPage=1&search=%2B5"; - var url = pagingModel.GetUri(defaultUrl); + var pagingModel = new PagingModel { PerPage = 1, Search = "+5" }; - Assert.Equal(expectedUrl, url); - } + var url = pagingModel.GetUri(defaultUrl); - [Fact(DisplayName = "Проверка возвращения Url без ? от DataTable.")] - public void ShouldReturnUrlWithoutQuestionMarkFromDataTable() - { - const string defaultUrl = "http://localhost:5005/api/systems/filter"; - const string expectedUrl = "http://localhost:5005/api/systems/filter?&page=1&sortCol=test&sortDir=asc&search=1"; - var pagingModel = new DataTable - { - Order = new[] { new OrderColumn { Column = 0, Dir = "asc" } }, - Columns = new[] { new DataColumn { Name = "test" } }, - Search = new DataSearch { Value = "1" } - }; - - var url = pagingModel.GetUri(defaultUrl); + Assert.Equal(expectedUrl, url); + } - Assert.Equal(expectedUrl, url); - } + [Fact(DisplayName = "Проверка возвращения Url без ? от PagingModel.")] + public void ShouldReturnUrlWithoutQuestionMarkFromPagingModel() + { + // TODO: Выяснить почему тут испольузется ?& + const string defaultUrl = "http://localhost:5005/api/systems/filter"; + const string expectedUrl = "http://localhost:5005/api/systems/filter?&page=1&perPage=2&search=1"; + var pagingModel = new PagingModel { PerPage = 2, Search = "1" }; - [Fact(DisplayName = "Проверка возвращения Url c ? от PagingModel.")] - public void ShouldReturnUrlWithQuestionMarkFromPagingModel() - { - const string defaultUrl = "http://localhost:5005/api/systems/filter?response=json"; - const string expectedUrl = "http://localhost:5005/api/systems/filter?response=json&page=1&perPage=2&search=1"; - var pagingModel = new PagingModel { PerPage = 2, Search = "1" }; + var url = pagingModel.GetUri(defaultUrl); - var url = pagingModel.GetUri(defaultUrl); + Assert.Equal(expectedUrl, url); + } - Assert.Equal(expectedUrl, url); - } - [Fact(DisplayName = "Проверка возвращения Url c ? от DataTable.")] - public void ShouldReturnUrlWithQuestionMarkFromDataTable() - { - const string defaultUrl = "http://localhost:5005/api/systems/filter?response=json"; - const string expectedUrl = "http://localhost:5005/api/systems/filter?response=json&page=1&sortCol=test&sortDir=asc&search=1"; - var pagingModel = new DataTable - { - Order = new[] { new OrderColumn { Column = 0, Dir = "asc" } }, - Columns = new[] { new DataColumn { Name = "test" } }, - Search = new DataSearch { Value = "1" } - }; + [Fact(DisplayName = "Проверка возвращения Url c ? от PagingModel.")] + public void ShouldReturnUrlWithQuestionMarkFromPagingModel() + { + const string defaultUrl = "http://localhost:5005/api/systems/filter?response=json"; + const string expectedUrl = "http://localhost:5005/api/systems/filter?response=json&page=1&perPage=2&search=1"; + var pagingModel = new PagingModel { PerPage = 2, Search = "1" }; - var url = pagingModel.GetUri(defaultUrl); + var url = pagingModel.GetUri(defaultUrl); - Assert.Equal(expectedUrl, url); - } + Assert.Equal(expectedUrl, url); } } diff --git a/src/Monq.Core.Paging.Tests/Monq.Core.Paging.Tests.csproj b/src/Monq.Core.Paging.Tests/Monq.Core.Paging.Tests.csproj index 207c8ec..a764bb7 100644 --- a/src/Monq.Core.Paging.Tests/Monq.Core.Paging.Tests.csproj +++ b/src/Monq.Core.Paging.Tests/Monq.Core.Paging.Tests.csproj @@ -1,7 +1,7 @@ - + - net7.0 + net8.0 @@ -10,13 +10,13 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/src/Monq.Core.Paging.Tests/MultiLevelPagingSortingTests.cs b/src/Monq.Core.Paging.Tests/MultiLevelPagingSortingTests.cs index 6e5f372..672b738 100644 --- a/src/Monq.Core.Paging.Tests/MultiLevelPagingSortingTests.cs +++ b/src/Monq.Core.Paging.Tests/MultiLevelPagingSortingTests.cs @@ -4,288 +4,287 @@ using System.Linq; using Xunit; -namespace Monq.Core.Paging.Tests +namespace Monq.Core.Paging.Tests; + +public class MultiLevelPagingSortingTests { - public class MultiLevelPagingSortingTests + static IEnumerable SeedData() { - static IEnumerable SeedData() + var data = new List { - var data = new List + new FirstLevel { - new FirstLevel + Id = 1, + Name = "FirstLevel1", + SecondLevel = new SecondLevel { - Id = 1, - Name = "FirstLevel1", - SecondLevel = new SecondLevel + ThirdLevel = new ThirdLevel { - ThirdLevel = new ThirdLevel - { - Id = 3, - Name = "ThirdLevel3" - } + Id = 3, + Name = "ThirdLevel3" } - }, - new FirstLevel + } + }, + new FirstLevel + { + Id = 2, + Name = "FirstLevel2", + SecondLevel = new SecondLevel { - Id = 2, - Name = "FirstLevel2", - SecondLevel = new SecondLevel + Name = "SecondLeve3", + ThirdLevel = new ThirdLevel { - Name = "SecondLeve3", - ThirdLevel = new ThirdLevel + Id = 1, + Name = "ThirdLevel1", + FourthLevel = new FourthLevel { - Id = 1, - Name = "ThirdLevel1", - FourthLevel = new FourthLevel - { - Enabled = false, - Name = "FourthLevel1", - Id = 1 - } + Enabled = false, + Name = "FourthLevel1", + Id = 1 } } - }, - new FirstLevel + } + }, + new FirstLevel + { + Id = 3, + Name = "FirstLevel3", + SecondLevel = new SecondLevel { - Id = 3, - Name = "FirstLevel3", - SecondLevel = new SecondLevel + Name = "SecondLeve2", + ThirdLevel = new ThirdLevel { - Name = "SecondLeve2", - ThirdLevel = new ThirdLevel + Id = 5, + Name = "ThirdLevel5", + FourthLevel = new FourthLevel { - Id = 5, - Name = "ThirdLevel5", - FourthLevel = new FourthLevel - { - Enabled = true, - Name = "FourthLevel3", - Id = 2 - } + Enabled = true, + Name = "FourthLevel3", + Id = 2 } } - }, - new FirstLevel + } + }, + new FirstLevel + { + Id = 4, + Name = "FirstLevel4", + SecondLevel = new SecondLevel { - Id = 4, - Name = "FirstLevel4", - SecondLevel = new SecondLevel + Id = 125, + Name = "SecondLevel", + ThirdLevel = new ThirdLevel { - Id = 125, - Name = "SecondLevel", - ThirdLevel = new ThirdLevel + Id = 5, + Name = "ThirdLevel4", + FourthLevel = new FourthLevel { - Id = 5, - Name = "ThirdLevel4", - FourthLevel = new FourthLevel - { - Enabled = true, - Name = "FourthLevel2", - Id = 3 - } + Enabled = true, + Name = "FourthLevel2", + Id = 3 } } } - }; - return data; - } + } + }; + return data; + } - [Fact(DisplayName = "Проверка многоуровневой сортировки записей. Проверка первого уровня, сортировка по наименованию (DESC).")] - public void ShouldProperlySortFirstLevelByName() - { - var paging = new Models.PagingModel - { - Page = 1, - PerPage = 10, - SortCol = "Name", - SortDir = "desc" - }; - - var httpContext = new DefaultHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("localhost", 5005); - httpContext.Request.Path = "/api/levels"; - - var levels = SeedData(); - var result = levels - .AsQueryable() - .WithPaging(paging, httpContext, x => x.Id); - - Assert.Equal(4, result.Count()); - - var firstObject = result.FirstOrDefault(); - var secondObject = result.Skip(1).FirstOrDefault(); - var thirdObject = result.Skip(2).FirstOrDefault(); - var fourthObject = result.Skip(3).FirstOrDefault(); - - Assert.Equal(4, firstObject.Id); - Assert.Equal(3, secondObject.Id); - Assert.Equal(2, thirdObject.Id); - Assert.Equal(1, fourthObject.Id); - } - - [Fact(DisplayName = "Проверка многоуровневой сортировки записей. Проверка второго уровня, сортировка по наименованию (DESC).")] - public void ShouldProperlySortSecondLevelByName() - { - var paging = new Models.PagingModel - { - Page = 1, - PerPage = 10, - SortCol = "SecondLevel.Name", - SortDir = "desc" - }; - - var httpContext = new DefaultHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("localhost", 5005); - httpContext.Request.Path = "/api/levels"; - - var levels = SeedData(); - var result = levels - .AsQueryable() - .WithPaging(paging, httpContext, x => x.Id); - - Assert.Equal(4, result.Count()); - - var firstObject = result.FirstOrDefault(); - var secondObject = result.Skip(1).FirstOrDefault(); - var thirdObject = result.Skip(2).FirstOrDefault(); - var fourthObject = result.Skip(3).FirstOrDefault(); - - Assert.Equal(4, firstObject.Id); - Assert.Equal(2, secondObject.Id); - Assert.Equal(3, thirdObject.Id); - Assert.Equal(1, fourthObject.Id); - } - - [Fact(DisplayName = "Проверка многоуровневой сортировки записей. Проверка третьего уровня, сортировка по наименованию (ASC).")] - public void ShouldProperlySortThirdLevelByName() - { - var paging = new Models.PagingModel - { - Page = 1, - PerPage = 10, - SortCol = "SecondLevel.ThirdLevel.Name ", - SortDir = "asc" - }; - - var httpContext = new DefaultHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("localhost", 5005); - httpContext.Request.Path = "/api/levels"; - - var levels = SeedData(); - var result = levels - .AsQueryable() - .WithPaging(paging, httpContext, x => x.Id); - - Assert.Equal(4, result.Count()); - - var firstObject = result.FirstOrDefault(); - var secondObject = result.Skip(1).FirstOrDefault(); - var thirdObject = result.Skip(2).FirstOrDefault(); - var fourthObject = result.Skip(3).FirstOrDefault(); - - Assert.Equal(2, firstObject.Id); - Assert.Equal(1, secondObject.Id); - Assert.Equal(4, thirdObject.Id); - Assert.Equal(3, fourthObject.Id); - } - - [Fact(DisplayName = "Проверка многоуровневой сортировки записей. Проверка четвертого уровня, сортировка по наименованию (ASC).")] - public void ShouldProperlySortFourthLevelByName() - { - var paging = new Models.PagingModel - { - Page = 1, - PerPage = 10, - SortCol = "SecondLevel.ThirdLevel.FourthLevel.Name", - SortDir = "asc" - }; - - var httpContext = new DefaultHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("localhost", 5005); - httpContext.Request.Path = "/api/levels"; - - var levels = SeedData(); - var result = levels - .AsQueryable() - .WithPaging(paging, httpContext, x => x.Id); - - Assert.Equal(4, result.Count()); - - var firstObject = result.FirstOrDefault(); - var secondObject = result.Skip(1).FirstOrDefault(); - var thirdObject = result.Skip(2).FirstOrDefault(); - var fourthObject = result.Skip(3).FirstOrDefault(); - - Assert.Equal(1, firstObject.Id); - Assert.Equal(2, secondObject.Id); - Assert.Equal(4, thirdObject.Id); - Assert.Equal(3, fourthObject.Id); - } - - [Fact(DisplayName = "Проверка многоуровневой сортировки записей. Проверка четвертого уровня, сортировка по enabled (ASC).")] - public void ShouldProperlySortFourthLevelByEnabled() + [Fact(DisplayName = "Проверка многоуровневой сортировки записей. Проверка первого уровня, сортировка по наименованию (DESC).")] + public void ShouldProperlySortFirstLevelByName() + { + var paging = new Models.PagingModel { - var paging = new Models.PagingModel - { - Page = 1, - PerPage = 10, - SortCol = "SecondLevel.ThirdLevel.FourthLevel.Enabled", - SortDir = "asc" - }; - - var httpContext = new DefaultHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("localhost", 5005); - httpContext.Request.Path = "/api/levels"; - - var levels = SeedData(); - var result = levels - .AsQueryable() - .WithPaging(paging, httpContext, x => x.Id); - - Assert.Equal(4, result.Count()); - - var firstObject = result.FirstOrDefault(); - var secondObject = result.Skip(1).FirstOrDefault(); - var thirdObject = result.Skip(2).FirstOrDefault(); - var fourthObject = result.Skip(3).FirstOrDefault(); - - Assert.Equal(1, firstObject.Id); - Assert.Equal(2, secondObject.Id); - Assert.Equal(3, thirdObject.Id); - Assert.Equal(4, fourthObject.Id); - } + Page = 1, + PerPage = 10, + SortCol = "Name", + SortDir = "desc" + }; + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("localhost", 5005); + httpContext.Request.Path = "/api/levels"; + + var levels = SeedData(); + var result = levels + .AsQueryable() + .WithPaging(paging, httpContext, x => x.Id); + + Assert.Equal(4, result.Count()); + + var firstObject = result.FirstOrDefault(); + var secondObject = result.Skip(1).FirstOrDefault(); + var thirdObject = result.Skip(2).FirstOrDefault(); + var fourthObject = result.Skip(3).FirstOrDefault(); + + Assert.Equal(4, firstObject.Id); + Assert.Equal(3, secondObject.Id); + Assert.Equal(2, thirdObject.Id); + Assert.Equal(1, fourthObject.Id); } - public class FirstLevel + [Fact(DisplayName = "Проверка многоуровневой сортировки записей. Проверка второго уровня, сортировка по наименованию (DESC).")] + public void ShouldProperlySortSecondLevelByName() { - public int Id { get; set; } - public string Name { get; set; } - public SecondLevel SecondLevel { get; set; } + var paging = new Models.PagingModel + { + Page = 1, + PerPage = 10, + SortCol = "SecondLevel.Name", + SortDir = "desc" + }; + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("localhost", 5005); + httpContext.Request.Path = "/api/levels"; + + var levels = SeedData(); + var result = levels + .AsQueryable() + .WithPaging(paging, httpContext, x => x.Id); + + Assert.Equal(4, result.Count()); + + var firstObject = result.FirstOrDefault(); + var secondObject = result.Skip(1).FirstOrDefault(); + var thirdObject = result.Skip(2).FirstOrDefault(); + var fourthObject = result.Skip(3).FirstOrDefault(); + + Assert.Equal(4, firstObject.Id); + Assert.Equal(2, secondObject.Id); + Assert.Equal(3, thirdObject.Id); + Assert.Equal(1, fourthObject.Id); } - public class SecondLevel + [Fact(DisplayName = "Проверка многоуровневой сортировки записей. Проверка третьего уровня, сортировка по наименованию (ASC).")] + public void ShouldProperlySortThirdLevelByName() { - public int Id { get; set; } - public string Name { get; set; } - public ThirdLevel ThirdLevel { get; set; } + var paging = new Models.PagingModel + { + Page = 1, + PerPage = 10, + SortCol = "SecondLevel.ThirdLevel.Name ", + SortDir = "asc" + }; + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("localhost", 5005); + httpContext.Request.Path = "/api/levels"; + + var levels = SeedData(); + var result = levels + .AsQueryable() + .WithPaging(paging, httpContext, x => x.Id); + + Assert.Equal(4, result.Count()); + + var firstObject = result.FirstOrDefault(); + var secondObject = result.Skip(1).FirstOrDefault(); + var thirdObject = result.Skip(2).FirstOrDefault(); + var fourthObject = result.Skip(3).FirstOrDefault(); + + Assert.Equal(2, firstObject.Id); + Assert.Equal(1, secondObject.Id); + Assert.Equal(4, thirdObject.Id); + Assert.Equal(3, fourthObject.Id); } - public class ThirdLevel + [Fact(DisplayName = "Проверка многоуровневой сортировки записей. Проверка четвертого уровня, сортировка по наименованию (ASC).")] + public void ShouldProperlySortFourthLevelByName() { - public int Id { get; set; } - public string Name { get; set; } - public FourthLevel FourthLevel { get; set; } + var paging = new Models.PagingModel + { + Page = 1, + PerPage = 10, + SortCol = "SecondLevel.ThirdLevel.FourthLevel.Name", + SortDir = "asc" + }; + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("localhost", 5005); + httpContext.Request.Path = "/api/levels"; + + var levels = SeedData(); + var result = levels + .AsQueryable() + .WithPaging(paging, httpContext, x => x.Id); + + Assert.Equal(4, result.Count()); + + var firstObject = result.FirstOrDefault(); + var secondObject = result.Skip(1).FirstOrDefault(); + var thirdObject = result.Skip(2).FirstOrDefault(); + var fourthObject = result.Skip(3).FirstOrDefault(); + + Assert.Equal(1, firstObject.Id); + Assert.Equal(2, secondObject.Id); + Assert.Equal(4, thirdObject.Id); + Assert.Equal(3, fourthObject.Id); } - public class FourthLevel + [Fact(DisplayName = "Проверка многоуровневой сортировки записей. Проверка четвертого уровня, сортировка по enabled (ASC).")] + public void ShouldProperlySortFourthLevelByEnabled() { - public int Id { get; set; } - public string Name { get; set; } - public bool Enabled { get; set; } + var paging = new Models.PagingModel + { + Page = 1, + PerPage = 10, + SortCol = "SecondLevel.ThirdLevel.FourthLevel.Enabled", + SortDir = "asc" + }; + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("localhost", 5005); + httpContext.Request.Path = "/api/levels"; + + var levels = SeedData(); + var result = levels + .AsQueryable() + .WithPaging(paging, httpContext, x => x.Id); + + Assert.Equal(4, result.Count()); + + var firstObject = result.FirstOrDefault(); + var secondObject = result.Skip(1).FirstOrDefault(); + var thirdObject = result.Skip(2).FirstOrDefault(); + var fourthObject = result.Skip(3).FirstOrDefault(); + + Assert.Equal(1, firstObject.Id); + Assert.Equal(2, secondObject.Id); + Assert.Equal(3, thirdObject.Id); + Assert.Equal(4, fourthObject.Id); } +} + +public class FirstLevel +{ + public int Id { get; set; } + public string Name { get; set; } + public SecondLevel SecondLevel { get; set; } +} + +public class SecondLevel +{ + public int Id { get; set; } + public string Name { get; set; } + public ThirdLevel ThirdLevel { get; set; } +} + +public class ThirdLevel +{ + public int Id { get; set; } + public string Name { get; set; } + public FourthLevel FourthLevel { get; set; } +} + +public class FourthLevel +{ + public int Id { get; set; } + public string Name { get; set; } + public bool Enabled { get; set; } } \ No newline at end of file diff --git a/src/Monq.Core.Paging.Tests/PagingExtensionsTests.cs b/src/Monq.Core.Paging.Tests/PagingExtensionsTests.cs index 7aa3c32..c3e039d 100644 --- a/src/Monq.Core.Paging.Tests/PagingExtensionsTests.cs +++ b/src/Monq.Core.Paging.Tests/PagingExtensionsTests.cs @@ -1,640 +1,369 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Monq.Core.Paging.Extensions; using Monq.Core.Paging.Models; -using Monq.Core.Paging.Models.DataTable; using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Xunit; -namespace Monq.Core.Paging.Tests +namespace Monq.Core.Paging.Tests; + +public class PagingExtensionsTests { - public class PagingExtensionsTests + class Build { - class Build - { - public int Id { get; set; } - public Guid GuidId { get; set; } - public string Name { get; set; } - public Build SubBuild { get; set; } - } - - static IEnumerable GenerateBuilds(int count = 1) - { - var builds = new List(); - for (var i = 0; i < count; i++) - { - var guidString = string.Format("{0:00000000-0000-0000-0000-000000000000}", 1 + i); - builds.Add(new Build { Id = i, Name = $"Сборка test_{i}", GuidId = new Guid(guidString) }); - } - - return builds; - } - - [Fact(DisplayName = "Проверка постраничной навигации для 4/8 записей, страница 1.")] - public void ShouldProperlyReturnDataWith4of8Page1() - { - var paging = new PagingModel { Page = 1, PerPage = 4 }; - - var httpContext = new DefaultHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("localhost", 5005); - httpContext.Request.Path = "/api/builds/byjobids"; - httpContext.Request.QueryString = new QueryString("?jobs=1,3"); - - var builds = GenerateBuilds(8); - var result = builds - .AsQueryable() - .WithPaging(paging, httpContext, x => x.Id) - .ToList(); - - Assert.Equal(4, result.Count); - - var linksHeader = (string)httpContext.Response.Headers["Link"]; - - Assert.False(string.IsNullOrEmpty(linksHeader)); - - var pageLinks = linksHeader.Replace("\",", "\"|").Split('|'); - - Assert.Equal(2, pageLinks.Length); - Assert.Equal("; rel=\"next\"", pageLinks[0]); - Assert.Equal("; rel=\"last\"", pageLinks[1]); - - Assert.Equal("8", httpContext.Response.Headers["X-Total"]); - Assert.Equal("8", httpContext.Response.Headers["X-Total-Filtered"]); - Assert.Equal("4", httpContext.Response.Headers["X-Per-Page"]); - Assert.Equal("1", httpContext.Response.Headers["X-Page"]); - Assert.Equal("2", httpContext.Response.Headers["X-Total-Pages"]); - } - - [Fact(DisplayName = "Проверка постраничной навигации c ответом DataTables для 4/8 записей, страница 1.")] - public async Task ShouldProperlyReturnDataTablesWith4of8Page1() - { - var paging = new Models.PagingModel { Page = 1, PerPage = 4 }; - - var httpContext = new DefaultHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("localhost", 5005); - httpContext.Request.Path = "/api/builds/byjobids"; - httpContext.Request.QueryString = new QueryString("?jobs=1,3"); - - var builds = new TestAsyncEnumerable( - GenerateBuilds(8)); - - var result = await builds - .AsQueryable() - .WithPaging(paging, httpContext, x => x.Id) - .ToDataTablesResponseAsync(httpContext); - - Assert.Equal(4, result.Data.Count()); - - var linksHeader = (string)httpContext.Response.Headers["Link"]; - - Assert.False(string.IsNullOrEmpty(linksHeader)); - - var pageLinks = linksHeader.Replace("\",", "\"|").Split('|'); - - Assert.Equal(2, pageLinks.Length); - Assert.Equal("; rel=\"next\"", pageLinks[0]); - Assert.Equal("; rel=\"last\"", pageLinks[1]); - - Assert.Equal("8", httpContext.Response.Headers["X-Total"]); - Assert.Equal("8", httpContext.Response.Headers["X-Total-Filtered"]); - Assert.Equal("4", httpContext.Response.Headers["X-Per-Page"]); - Assert.Equal("1", httpContext.Response.Headers["X-Page"]); - Assert.Equal("2", httpContext.Response.Headers["X-Total-Pages"]); - } - - [Fact(DisplayName = "Проверка постраничной навигации для 4/12 записей, страница 2.")] - public void ShouldProperlyReturnDataWith4of12Page2() - { - var paging = new Models.PagingModel { Page = 2, PerPage = 4 }; - - var httpContext = new DefaultHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("localhost", 5005); - httpContext.Request.Path = "/api/builds/byjobids"; - httpContext.Request.QueryString = new QueryString("?jobs=1,3"); - - var builds = GenerateBuilds(12); - var result = builds - .AsQueryable() - .WithPaging(paging, httpContext, x => x.Id) - .ToList(); - - Assert.Equal(4, result.Count); - - var linksHeader = (string)httpContext.Response.Headers["Link"]; - - Assert.False(string.IsNullOrEmpty(linksHeader)); - - var pageLinks = linksHeader.Replace("\",", "\"|").Split('|'); - - Assert.Equal(4, pageLinks.Length); - Assert.Equal("; rel=\"first\"", pageLinks[0]); - Assert.Equal("; rel=\"prev\"", pageLinks[1]); - Assert.Equal("; rel=\"next\"", pageLinks[2]); - Assert.Equal("; rel=\"last\"", pageLinks[3]); - - Assert.Equal("12", httpContext.Response.Headers["X-Total"]); - Assert.Equal("12", httpContext.Response.Headers["X-Total-Filtered"]); - Assert.Equal("4", httpContext.Response.Headers["X-Per-Page"]); - Assert.Equal("2", httpContext.Response.Headers["X-Page"]); - Assert.Equal("3", httpContext.Response.Headers["X-Total-Pages"]); - } + public int Id { get; set; } + public Guid GuidId { get; set; } + public string Name { get; set; } + public Build SubBuild { get; set; } + } - [Fact(DisplayName = "Проверка постраничной навигации с ответом DataTables для 4/12 записей, страница 2.")] - public async Task ShouldProperlyReturnDataTablesWith4of12Page2() + static IEnumerable GenerateBuilds(int count = 1) + { + var builds = new List(); + for (var i = 0; i < count; i++) { - var paging = new Models.PagingModel { Page = 2, PerPage = 4 }; - - var httpContext = new DefaultHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("localhost", 5005); - httpContext.Request.Path = "/api/builds/byjobids"; - httpContext.Request.QueryString = new QueryString("?jobs=1,3"); - - var builds = new TestAsyncEnumerable( - GenerateBuilds(12)); - var result = await builds - .AsQueryable() - .WithPaging(paging, httpContext, x => x.Id) - .ToDataTablesResponseAsync(httpContext); - - Assert.Equal(4, result.Data.Count()); - - var linksHeader = (string)httpContext.Response.Headers["Link"]; - - Assert.False(string.IsNullOrEmpty(linksHeader)); - - var pageLinks = linksHeader.Replace("\",", "\"|").Split('|'); - - Assert.Equal(4, pageLinks.Length); - Assert.Equal("; rel=\"first\"", pageLinks[0]); - Assert.Equal("; rel=\"prev\"", pageLinks[1]); - Assert.Equal("; rel=\"next\"", pageLinks[2]); - Assert.Equal("; rel=\"last\"", pageLinks[3]); - - Assert.Equal("12", httpContext.Response.Headers["X-Total"]); - Assert.Equal("12", httpContext.Response.Headers["X-Total-Filtered"]); - Assert.Equal("4", httpContext.Response.Headers["X-Per-Page"]); - Assert.Equal("2", httpContext.Response.Headers["X-Page"]); - Assert.Equal("3", httpContext.Response.Headers["X-Total-Pages"]); + var guidString = string.Format("{0:00000000-0000-0000-0000-000000000000}", 1 + i); + builds.Add(new Build { Id = i, Name = $"Сборка test_{i}", GuidId = new Guid(guidString) }); } - [Fact(DisplayName = "Проверка постраничной навигации для Page > TotalPages. 4/8 записей, страница 1.")] - public void ShouldProperlyReturnDataWithPageMoreThanTotalPages() - { - var paging = new Models.PagingModel { Page = 10, PerPage = 4 }; - - var httpContext = new DefaultHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("localhost", 5005); - httpContext.Request.Path = "/api/builds/byjobids"; - httpContext.Request.QueryString = new QueryString("?jobs=1,3"); - - var builds = GenerateBuilds(8); - var result = builds - .AsQueryable() - .WithPaging(paging, httpContext, x => x.Id) - .ToList(); - - Assert.Empty(result); - - var linksHeader = (string)httpContext.Response.Headers["Link"]; - - Assert.False(string.IsNullOrEmpty(linksHeader)); - - var pageLinks = linksHeader.Replace("\",", "\"|").Split('|'); - - Assert.Equal(2, pageLinks.Length); - Assert.Equal("; rel=\"first\"", pageLinks[0]); - Assert.Equal("; rel=\"prev\"", pageLinks[1]); - - Assert.Equal("8", httpContext.Response.Headers["X-Total"]); - Assert.Equal("8", httpContext.Response.Headers["X-Total-Filtered"]); - Assert.Equal("4", httpContext.Response.Headers["X-Per-Page"]); - Assert.Equal("10", httpContext.Response.Headers["X-Page"]); - Assert.Equal("2", httpContext.Response.Headers["X-Total-Pages"]); - } + return builds; + } - [Fact(DisplayName = "Проверка постраничной навигации с ответом DataTables для Page > TotalPages. 4/8 записей, страница 1.")] - public async Task ShouldProperlyReturnDataTablesWithPageMoreThanTotalPages() - { - var paging = new Models.PagingModel { Page = 10, PerPage = 4 }; + [Fact(DisplayName = "Проверка постраничной навигации для 4/8 записей, страница 1.")] + public void ShouldProperlyReturnDataWith4of8Page1() + { + var paging = new PagingModel { Page = 1, PerPage = 4 }; - var httpContext = new DefaultHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("localhost", 5005); - httpContext.Request.Path = "/api/builds/byjobids"; - httpContext.Request.QueryString = new QueryString("?jobs=1,3"); + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("localhost", 5005); + httpContext.Request.Path = "/api/builds/byjobids"; + httpContext.Request.QueryString = new QueryString("?jobs=1,3"); - var builds = new TestAsyncEnumerable( - GenerateBuilds(8)); - var result = await builds - .AsQueryable() - .WithPaging(paging, httpContext, x => x.Id) - .ToDataTablesResponseAsync(httpContext); + var builds = GenerateBuilds(8); + var result = builds + .AsQueryable() + .WithPaging(paging, httpContext, x => x.Id) + .ToList(); - Assert.Empty(result.Data); + Assert.Equal(4, result.Count); - var linksHeader = (string)httpContext.Response.Headers["Link"]; + var linksHeader = (string)httpContext.Response.Headers["Link"]; - Assert.False(string.IsNullOrEmpty(linksHeader)); + Assert.False(string.IsNullOrEmpty(linksHeader)); - var pageLinks = linksHeader.Replace("\",", "\"|").Split('|'); + var pageLinks = linksHeader.Replace("\",", "\"|").Split('|'); - Assert.Equal(2, pageLinks.Length); - Assert.Equal("; rel=\"first\"", pageLinks[0]); - Assert.Equal("; rel=\"prev\"", pageLinks[1]); + Assert.Equal(2, pageLinks.Length); + Assert.Equal("; rel=\"next\"", pageLinks[0]); + Assert.Equal("; rel=\"last\"", pageLinks[1]); - Assert.Equal("8", httpContext.Response.Headers["X-Total"]); - Assert.Equal("8", httpContext.Response.Headers["X-Total-Filtered"]); - Assert.Equal("4", httpContext.Response.Headers["X-Per-Page"]); - Assert.Equal("10", httpContext.Response.Headers["X-Page"]); - Assert.Equal("2", httpContext.Response.Headers["X-Total-Pages"]); - } + Assert.Equal("8", httpContext.Response.Headers["X-Total"]); + Assert.Equal("8", httpContext.Response.Headers["X-Total-Filtered"]); + Assert.Equal("4", httpContext.Response.Headers["X-Per-Page"]); + Assert.Equal("1", httpContext.Response.Headers["X-Page"]); + Assert.Equal("2", httpContext.Response.Headers["X-Total-Pages"]); + } - [Fact(DisplayName = "Проверка постраничной навигации для Page = -1. 4/8 записей, страница 1.")] - public void ShouldProperlyReturnDataWithPageMinusOne() - { - var paging = new Models.PagingModel { Page = 2, PerPage = -1 }; + [Fact(DisplayName = "Проверка постраничной навигации для 4/12 записей, страница 2.")] + public void ShouldProperlyReturnDataWith4of12Page2() + { + var paging = new Models.PagingModel { Page = 2, PerPage = 4 }; - var httpContext = new DefaultHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("localhost", 5005); - httpContext.Request.Path = "/api/builds/byjobids"; - httpContext.Request.QueryString = new QueryString("?jobs=1,3"); + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("localhost", 5005); + httpContext.Request.Path = "/api/builds/byjobids"; + httpContext.Request.QueryString = new QueryString("?jobs=1,3"); - var builds = GenerateBuilds(8); - var result = builds - .AsQueryable() - .WithPaging(paging, httpContext, x => x.Id) - .ToList(); + var builds = GenerateBuilds(12); + var result = builds + .AsQueryable() + .WithPaging(paging, httpContext, x => x.Id) + .ToList(); - Assert.Equal(8, result.Count); + Assert.Equal(4, result.Count); - var linksHeader = (string)httpContext.Response.Headers["Link"]; + var linksHeader = (string)httpContext.Response.Headers["Link"]; - Assert.True(string.IsNullOrEmpty(linksHeader)); + Assert.False(string.IsNullOrEmpty(linksHeader)); - var pageLinks = linksHeader.Replace("\",", "\"|").Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + var pageLinks = linksHeader.Replace("\",", "\"|").Split('|'); - Assert.Empty(pageLinks); + Assert.Equal(4, pageLinks.Length); + Assert.Equal("; rel=\"first\"", pageLinks[0]); + Assert.Equal("; rel=\"prev\"", pageLinks[1]); + Assert.Equal("; rel=\"next\"", pageLinks[2]); + Assert.Equal("; rel=\"last\"", pageLinks[3]); - Assert.Equal("8", httpContext.Response.Headers["X-Total"]); - Assert.Equal("8", httpContext.Response.Headers["X-Total-Filtered"]); - Assert.Equal("-1", httpContext.Response.Headers["X-Per-Page"]); - Assert.Equal("1", httpContext.Response.Headers["X-Page"]); - Assert.Equal("1", httpContext.Response.Headers["X-Total-Pages"]); - } + Assert.Equal("12", httpContext.Response.Headers["X-Total"]); + Assert.Equal("12", httpContext.Response.Headers["X-Total-Filtered"]); + Assert.Equal("4", httpContext.Response.Headers["X-Per-Page"]); + Assert.Equal("2", httpContext.Response.Headers["X-Page"]); + Assert.Equal("3", httpContext.Response.Headers["X-Total-Pages"]); + } - [Fact(DisplayName = "Проверка постраничной навигации c ответом DataTables для Page = -1. 4/8 записей, страница 1.")] - public async Task ShouldProperlyReturnDataTablesWithPageMinusOne() - { - var paging = new Models.PagingModel { Page = 2, PerPage = -1 }; + [Fact(DisplayName = "Проверка постраничной навигации для Page > TotalPages. 4/8 записей, страница 1.")] + public void ShouldProperlyReturnDataWithPageMoreThanTotalPages() + { + var paging = new Models.PagingModel { Page = 10, PerPage = 4 }; - var httpContext = new DefaultHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("localhost", 5005); - httpContext.Request.Path = "/api/builds/byjobids"; - httpContext.Request.QueryString = new QueryString("?jobs=1,3"); + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("localhost", 5005); + httpContext.Request.Path = "/api/builds/byjobids"; + httpContext.Request.QueryString = new QueryString("?jobs=1,3"); - var builds = new TestAsyncEnumerable( - GenerateBuilds(8)); - var result = await builds - .AsQueryable() - .WithPaging(paging, httpContext, x => x.Id) - .ToDataTablesResponseAsync(httpContext); + var builds = GenerateBuilds(8); + var result = builds + .AsQueryable() + .WithPaging(paging, httpContext, x => x.Id) + .ToList(); - Assert.Equal(8, result.Data.Count()); + Assert.Empty(result); - var linksHeader = (string)httpContext.Response.Headers["Link"]; + var linksHeader = (string)httpContext.Response.Headers["Link"]; - Assert.True(string.IsNullOrEmpty(linksHeader)); + Assert.False(string.IsNullOrEmpty(linksHeader)); - var pageLinks = linksHeader.Replace("\",", "\"|").Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + var pageLinks = linksHeader.Replace("\",", "\"|").Split('|'); - Assert.Empty(pageLinks); + Assert.Equal(2, pageLinks.Length); + Assert.Equal("; rel=\"first\"", pageLinks[0]); + Assert.Equal("; rel=\"prev\"", pageLinks[1]); - Assert.Equal("8", httpContext.Response.Headers["X-Total"]); - Assert.Equal("8", httpContext.Response.Headers["X-Total-Filtered"]); - Assert.Equal("-1", httpContext.Response.Headers["X-Per-Page"]); - Assert.Equal("1", httpContext.Response.Headers["X-Page"]); - Assert.Equal("1", httpContext.Response.Headers["X-Total-Pages"]); - } + Assert.Equal("8", httpContext.Response.Headers["X-Total"]); + Assert.Equal("8", httpContext.Response.Headers["X-Total-Filtered"]); + Assert.Equal("4", httpContext.Response.Headers["X-Per-Page"]); + Assert.Equal("10", httpContext.Response.Headers["X-Page"]); + Assert.Equal("2", httpContext.Response.Headers["X-Total-Pages"]); + } - [Fact(DisplayName = "Проверка постраничной навигации для Page <= 0. 4/8 записей, страница 1.")] - public void ShouldProperlyReturnDataWithPageLessThanZero() - { - var paging = new Models.PagingModel { Page = -2, PerPage = 4 }; + [Fact(DisplayName = "Проверка постраничной навигации для Page = -1. 4/8 записей, страница 1.")] + public void ShouldProperlyReturnDataWithPageMinusOne() + { + var paging = new Models.PagingModel { Page = 2, PerPage = -1 }; - var httpContext = new DefaultHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("localhost", 5005); - httpContext.Request.Path = "/api/builds/byjobids"; - httpContext.Request.QueryString = new QueryString("?jobs=1,3"); + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("localhost", 5005); + httpContext.Request.Path = "/api/builds/byjobids"; + httpContext.Request.QueryString = new QueryString("?jobs=1,3"); - var builds = GenerateBuilds(8); - var result = builds - .AsQueryable() - .WithPaging(paging, httpContext, x => x.Id) - .ToList(); + var builds = GenerateBuilds(8); + var result = builds + .AsQueryable() + .WithPaging(paging, httpContext, x => x.Id) + .ToList(); - Assert.Empty(result); + Assert.Equal(8, result.Count); - var linksHeader = (string)httpContext.Response.Headers["Link"]; + var linksHeader = (string)httpContext.Response.Headers["Link"]; - Assert.False(string.IsNullOrEmpty(linksHeader)); + Assert.True(string.IsNullOrEmpty(linksHeader)); - var pageLinks = linksHeader.Replace("\",", "\"|").Split('|'); + var pageLinks = linksHeader.Replace("\",", "\"|").Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries); - Assert.Equal(2, pageLinks.Length); - Assert.Equal("; rel=\"next\"", pageLinks[0]); - Assert.Equal("; rel=\"last\"", pageLinks[1]); + Assert.Empty(pageLinks); - Assert.Equal("8", httpContext.Response.Headers["X-Total"]); - Assert.Equal("8", httpContext.Response.Headers["X-Total-Filtered"]); - Assert.Equal("4", httpContext.Response.Headers["X-Per-Page"]); - Assert.Equal("-2", httpContext.Response.Headers["X-Page"]); - Assert.Equal("2", httpContext.Response.Headers["X-Total-Pages"]); - } + Assert.Equal("8", httpContext.Response.Headers["X-Total"]); + Assert.Equal("8", httpContext.Response.Headers["X-Total-Filtered"]); + Assert.Equal("-1", httpContext.Response.Headers["X-Per-Page"]); + Assert.Equal("1", httpContext.Response.Headers["X-Page"]); + Assert.Equal("1", httpContext.Response.Headers["X-Total-Pages"]); + } - [Fact(DisplayName = "Проверка постраничной навигации c ответом DataTables для Page <= 0. 4/8 записей, страница 1.")] - public async Task ShouldProperlyReturnDataTablesWithPageLessThanZero() - { - var paging = new Models.PagingModel { Page = -2, PerPage = 4 }; + [Fact(DisplayName = "Проверка постраничной навигации для Page <= 0. 4/8 записей, страница 1.")] + public void ShouldProperlyReturnDataWithPageLessThanZero() + { + var paging = new Models.PagingModel { Page = -2, PerPage = 4 }; - var httpContext = new DefaultHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("localhost", 5005); - httpContext.Request.Path = "/api/builds/byjobids"; - httpContext.Request.QueryString = new QueryString("?jobs=1,3"); + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("localhost", 5005); + httpContext.Request.Path = "/api/builds/byjobids"; + httpContext.Request.QueryString = new QueryString("?jobs=1,3"); - var builds = new TestAsyncEnumerable( - GenerateBuilds(8)); - var result = await builds - .AsQueryable() - .WithPaging(paging, httpContext, x => x.Id) - .ToDataTablesResponseAsync(httpContext); + var builds = GenerateBuilds(8); + var result = builds + .AsQueryable() + .WithPaging(paging, httpContext, x => x.Id) + .ToList(); - Assert.Empty(result.Data); + Assert.Empty(result); - var linksHeader = (string)httpContext.Response.Headers["Link"]; + var linksHeader = (string)httpContext.Response.Headers["Link"]; - Assert.False(string.IsNullOrEmpty(linksHeader)); + Assert.False(string.IsNullOrEmpty(linksHeader)); - var pageLinks = linksHeader.Replace("\",", "\"|").Split('|'); + var pageLinks = linksHeader.Replace("\",", "\"|").Split('|'); - Assert.Equal(2, pageLinks.Length); - Assert.Equal("; rel=\"next\"", pageLinks[0]); - Assert.Equal("; rel=\"last\"", pageLinks[1]); + Assert.Equal(2, pageLinks.Length); + Assert.Equal("; rel=\"next\"", pageLinks[0]); + Assert.Equal("; rel=\"last\"", pageLinks[1]); - Assert.Equal("8", httpContext.Response.Headers["X-Total"]); - Assert.Equal("8", httpContext.Response.Headers["X-Total-Filtered"]); - Assert.Equal("4", httpContext.Response.Headers["X-Per-Page"]); - Assert.Equal("-2", httpContext.Response.Headers["X-Page"]); - Assert.Equal("2", httpContext.Response.Headers["X-Total-Pages"]); - } + Assert.Equal("8", httpContext.Response.Headers["X-Total"]); + Assert.Equal("8", httpContext.Response.Headers["X-Total-Filtered"]); + Assert.Equal("4", httpContext.Response.Headers["X-Per-Page"]); + Assert.Equal("-2", httpContext.Response.Headers["X-Page"]); + Assert.Equal("2", httpContext.Response.Headers["X-Total-Pages"]); + } - [Fact(DisplayName = "Проверка фильтрации элементов по полю paging.Search по полю типа string")] - public void ShouldProperlySearchRecordsInListByStringField() - { - var paging = new Models.PagingModel { Page = 1, PerPage = 10, Search = "3" }; - - var httpContext = new DefaultHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("localhost", 5005); - httpContext.Request.Path = "/api/builds/byjobids"; - httpContext.Request.QueryString = new QueryString("?jobs=1,3"); - - var builds = GenerateBuilds(8); - var result = builds - .AsQueryable() - .WithPaging(paging, httpContext, x => x.Id, searchType: SearchType.Include, searchProps: x => x.Name) - .ToList(); - - Assert.Single(result); - Assert.Equal(3, result[0].Id); - } + [Fact(DisplayName = "Проверка фильтрации элементов по полю paging.Search по полю типа string")] + public void ShouldProperlySearchRecordsInListByStringField() + { + var paging = new Models.PagingModel { Page = 1, PerPage = 10, Search = "3" }; + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("localhost", 5005); + httpContext.Request.Path = "/api/builds/byjobids"; + httpContext.Request.QueryString = new QueryString("?jobs=1,3"); + + var builds = GenerateBuilds(8); + var result = builds + .AsQueryable() + .WithPaging(paging, httpContext, x => x.Id, searchType: SearchType.Include, searchProps: x => x.Name) + .ToList(); + + Assert.Single(result); + Assert.Equal(3, result[0].Id); + } - [Fact(DisplayName = "Проверка фильтрации c ответом DataTables элементов по полю paging.Search по полю типа string")] - public async Task ShouldProperlySearchDataTablesRecordsInListByStringField() - { - var paging = new Models.PagingModel { Page = 1, PerPage = 10, Search = "3" }; - - var httpContext = new DefaultHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("localhost", 5005); - httpContext.Request.Path = "/api/builds/byjobids"; - httpContext.Request.QueryString = new QueryString("?jobs=1,3"); - - var builds = new TestAsyncEnumerable( - GenerateBuilds(8)); - var result = await builds - .AsQueryable() - .WithPaging(paging, httpContext, x => x.Id, searchType: SearchType.Include, searchProps: x => x.Name) - .ToDataTablesResponseAsync(httpContext); - - Assert.Single(result.Data); - Assert.Equal(3, result.Data.First().Id); - } + [Fact(DisplayName = "Checking a filtration by property paging.Search (property type is Guid).")] + public void ShouldProperlySearchRecordsInListByGuidField() + { + var guidValue = new Guid(string.Format("{0:00000000-0000-0000-0000-000000000000}", 7)); + var paging = new Models.PagingModel { Page = 1, PerPage = 10, Search = guidValue.ToString() }; + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("localhost", 5005); + httpContext.Request.Path = "/api/builds/byjobids"; + httpContext.Request.QueryString = new QueryString("?jobs=1,3"); + + var builds = GenerateBuilds(8); + foreach (var build in builds) + build.Name = "build"; + + var result = builds + .AsQueryable() + .WithPaging(paging, httpContext, x => x.Id, searchType: SearchType.Include, searchProps: x => x.GuidId) + .ToList(); + + Assert.Single(result); + Assert.Equal(guidValue, result[0].GuidId); + Assert.Equal(6, result[0].Id); + } - [Fact(DisplayName = "Checking a filtration by property paging.Search (property type is Guid).")] - public void ShouldProperlySearchRecordsInListByGuidField() - { - var guidValue = new Guid(string.Format("{0:00000000-0000-0000-0000-000000000000}", 7)); - var paging = new Models.PagingModel { Page = 1, PerPage = 10, Search = guidValue.ToString() }; - - var httpContext = new DefaultHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("localhost", 5005); - httpContext.Request.Path = "/api/builds/byjobids"; - httpContext.Request.QueryString = new QueryString("?jobs=1,3"); - - var builds = GenerateBuilds(8); - foreach (var build in builds) - build.Name = "build"; - - var result = builds - .AsQueryable() - .WithPaging(paging, httpContext, x => x.Id, searchType: SearchType.Include, searchProps: x => x.GuidId) - .ToList(); - - Assert.Single(result); - Assert.Equal(guidValue, result[0].GuidId); - Assert.Equal(6, result[0].Id); - } + [Fact(DisplayName = "Checking a filtration paging.Search by several property (search value is number).")] + public void ShouldProperlySearchRecordsInListByManyFieldWhenSearchValueIsNumber() + { + var paging = new Models.PagingModel { Page = 1, PerPage = 10, Search = 4.ToString() }; - [Fact(DisplayName = "Checking a filtration paging.Search by several property (search value is number).")] - public void ShouldProperlySearchRecordsInListByManyFieldWhenSearchValueIsNumber() - { - var paging = new Models.PagingModel { Page = 1, PerPage = 10, Search = 4.ToString() }; + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("localhost", 5005); + httpContext.Request.Path = "/api/builds/byjobids"; + httpContext.Request.QueryString = new QueryString("?jobs=1,3"); - var httpContext = new DefaultHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("localhost", 5005); - httpContext.Request.Path = "/api/builds/byjobids"; - httpContext.Request.QueryString = new QueryString("?jobs=1,3"); + var builds = GenerateBuilds(8); - var builds = GenerateBuilds(8); + var result = builds + .AsQueryable() + .WithPaging(paging, httpContext, x => x.Id, null, SearchType.Include, 1, x => x.Id, x => x.GuidId) + .ToList(); - var result = builds - .AsQueryable() - .WithPaging(paging, httpContext, x => x.Id, null, SearchType.Include, 1, x => x.Id, x => x.GuidId) - .ToList(); + Assert.Equal(2, result.Count); + Assert.Equal(3, result[0].Id); + Assert.Equal(4, result[1].Id); + } - Assert.Equal(2, result.Count); - Assert.Equal(3, result[0].Id); - Assert.Equal(4, result[1].Id); - } + [Fact(DisplayName = "Checking a filtration paging.Search by several property (search value is string).")] + public void ShouldProperlySearchRecordsInListByManyFieldWhenSearchValueIsString() + { + var paging = new Models.PagingModel { Page = 1, PerPage = 10, Search = "abc123" }; - [Fact(DisplayName = "Checking a filtration paging.Search by several property (search value is string).")] - public void ShouldProperlySearchRecordsInListByManyFieldWhenSearchValueIsString() - { - var paging = new Models.PagingModel { Page = 1, PerPage = 10, Search = "abc123" }; - - var httpContext = new DefaultHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("localhost", 5005); - httpContext.Request.Path = "/api/builds/byjobids"; - httpContext.Request.QueryString = new QueryString("?jobs=1,3"); - - var builds = GenerateBuilds(8).Union(new[] - { - new Build { Id = 9, GuidId = new Guid(string.Format("{0:00000000-0000-0000-0000-00abc1230000}", 9))}, - new Build { Id = 10, GuidId = new Guid(string.Format("{0:00000000-0000-0000-0000-000000000000}", 10)), Name = "build-abc123"}, - }); - - var result = builds - .AsQueryable() - .WithPaging(paging, httpContext, x => x.Id, null, SearchType.Include, 1, x => x.Name, x => x.GuidId) - .ToList(); - - Assert.Equal(2, result.Count); - Assert.Equal(9, result[0].Id); - Assert.Equal(10, result[1].Id); - } + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("localhost", 5005); + httpContext.Request.Path = "/api/builds/byjobids"; + httpContext.Request.QueryString = new QueryString("?jobs=1,3"); - [Fact(DisplayName = "Проверка фильтрации элементов по полю paging.Search по полю типа int")] - public void ShouldProperlySearchRecordsInListByIntField() + var builds = GenerateBuilds(8).Union(new[] { - var paging = new Models.PagingModel { Page = 1, PerPage = 10, Search = "3" }; - - var httpContext = new DefaultHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("localhost", 5005); - httpContext.Request.Path = "/api/builds/byjobids"; - httpContext.Request.QueryString = new QueryString("?jobs=1,3"); + new Build { Id = 9, GuidId = new Guid(string.Format("{0:00000000-0000-0000-0000-00abc1230000}", 9))}, + new Build { Id = 10, GuidId = new Guid(string.Format("{0:00000000-0000-0000-0000-000000000000}", 10)), Name = "build-abc123"}, + }); + + var result = builds + .AsQueryable() + .WithPaging(paging, httpContext, x => x.Id, null, SearchType.Include, 1, x => x.Name, x => x.GuidId) + .ToList(); + + Assert.Equal(2, result.Count); + Assert.Equal(9, result[0].Id); + Assert.Equal(10, result[1].Id); + } - var builds = GenerateBuilds(8); - foreach (var build in builds) - build.Name = "build"; + [Fact(DisplayName = "Проверка фильтрации элементов по полю paging.Search по полю типа int")] + public void ShouldProperlySearchRecordsInListByIntField() + { + var paging = new Models.PagingModel { Page = 1, PerPage = 10, Search = "3" }; - var result = builds - .AsQueryable() - .WithPaging(paging, httpContext, x => x.Id, searchType: SearchType.Include, searchProps: x => x.Id) - .ToList(); + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("localhost", 5005); + httpContext.Request.Path = "/api/builds/byjobids"; + httpContext.Request.QueryString = new QueryString("?jobs=1,3"); - Assert.Single(result); - Assert.Equal(3, result[0].Id); - } + var builds = GenerateBuilds(8); + foreach (var build in builds) + build.Name = "build"; - [Fact(DisplayName = "Проверка фильтрации c ответом DataTables элементов по полю paging.Search по полю типа int")] - public async Task ShouldProperlySearchDataTablesRecordsInListByIntField() - { - var paging = new Models.PagingModel { Page = 1, PerPage = 10, Search = "3" }; - - var httpContext = new DefaultHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("localhost", 5005); - httpContext.Request.Path = "/api/builds/byjobids"; - httpContext.Request.QueryString = new QueryString("?jobs=1,3"); - - var builds = GenerateBuilds(8); - foreach (var build in builds) - build.Name = "build"; - var asyncBuilds = new TestAsyncEnumerable(builds); - - var result = await asyncBuilds - .AsQueryable() - .WithPaging(paging, httpContext, x => x.Id, searchType: SearchType.Include, searchProps: x => x.Id) - .ToDataTablesResponseAsync(httpContext); - - Assert.Single(result.Data); - Assert.Equal(3, result.Data.First().Id); - } + var result = builds + .AsQueryable() + .WithPaging(paging, httpContext, x => x.Id, searchType: SearchType.Include, searchProps: x => x.Id) + .ToList(); - [Fact(DisplayName = "Проверка фильтрации элементов по полю paging.Search с полями исключенными из поиска")] - public void ShouldProperlySearchRecordsInListWithExcludeSearchPropertiesDataTable() - { - var dataTable = new DataTable - { - Page = 1, - Length = 10, - Search = new DataSearch { Value = "3" } - }; - - var httpContext = CreateDefaultHttpContext(); - - var builds = Enumerable.Range(0, 10).Select(x => new Build { Id = x, Name = $"TestName{x + 1}" }); - var result = builds - .AsQueryable() - .WithPaging(dataTable, httpContext, x => x.Id) - .ToList(); - - Assert.Equal(2, result.Count); - - dataTable.Columns = new List - { - new DataColumn { Data="id",Searchable=false} - }; - - httpContext = CreateDefaultHttpContext(); - result = builds - .AsQueryable() - .WithPaging(dataTable, httpContext, x => x.Id) - .ToList(); - - Assert.Single(result); - Assert.Contains('3', result[0].Name); - } + Assert.Single(result); + Assert.Equal(3, result[0].Id); + } - [Fact(DisplayName = "Проверка фильтрации элементов по полю paging.Search с полями исключенными из поиска")] - public void ShouldProperlySearchRecordsInListWithExcludeSearchProperties() - { - var paging = new PagingModel { Page = 1, PerPage = 10, Search = "3" }; + [Fact(DisplayName = "Проверка фильтрации элементов по полю paging.Search с полями исключенными из поиска")] + public void ShouldProperlySearchRecordsInListWithExcludeSearchProperties() + { + var paging = new PagingModel { Page = 1, PerPage = 10, Search = "3" }; - var httpContext = CreateDefaultHttpContext(); + var httpContext = CreateDefaultHttpContext(); - var builds = Enumerable.Range(0, 10).Select(x => new Build { Id = x, Name = $"TestName{x + 1}" }).Union(new[] { new Build { Id = 11, Name = null } }); - var result = builds - .AsQueryable() - .WithPaging(paging, httpContext, x => x.Id) - .ToList(); + var builds = Enumerable.Range(0, 10).Select(x => new Build { Id = x, Name = $"TestName{x + 1}" }).Union(new[] { new Build { Id = 11, Name = null } }); + var result = builds + .AsQueryable() + .WithPaging(paging, httpContext, x => x.Id) + .ToList(); - Assert.Equal(2, result.Count); + Assert.Equal(2, result.Count); - httpContext = CreateDefaultHttpContext(); + httpContext = CreateDefaultHttpContext(); - result = builds - .AsQueryable() - .WithPaging(paging, httpContext, x => x.Id, null, SearchType.Exclude, 3, x => x.Id, x => x.SubBuild.SubBuild.Name) - .ToList(); + result = builds + .AsQueryable() + .WithPaging(paging, httpContext, x => x.Id, null, SearchType.Exclude, 3, x => x.Id, x => x.SubBuild.SubBuild.Name) + .ToList(); - Assert.Single(result); - Assert.Contains('3', result[0].Name); - } + Assert.Single(result); + Assert.Contains('3', result[0].Name); + } - static DefaultHttpContext CreateDefaultHttpContext() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("localhost", 5005); - httpContext.Request.Path = "/api/builds/byjobids"; - httpContext.Request.QueryString = new QueryString("?jobs=1,3"); - return httpContext; - } + static DefaultHttpContext CreateDefaultHttpContext() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("localhost", 5005); + httpContext.Request.Path = "/api/builds/byjobids"; + httpContext.Request.QueryString = new QueryString("?jobs=1,3"); + return httpContext; } -} \ No newline at end of file +} diff --git a/src/Monq.Core.Paging.Tests/PagingHelperTests.cs b/src/Monq.Core.Paging.Tests/PagingHelperTests.cs index 14b5829..3277e56 100644 --- a/src/Monq.Core.Paging.Tests/PagingHelperTests.cs +++ b/src/Monq.Core.Paging.Tests/PagingHelperTests.cs @@ -2,89 +2,88 @@ using System; using Xunit; -namespace Monq.Core.Paging.Tests +namespace Monq.Core.Paging.Tests; + +public class PagingHelperTests { - public class PagingHelperTests + [Fact] + public void ShouldProperlyReturnLinksPage2Of4() { - [Fact] - public void ShouldProperlyReturnLinksPage2Of4() - { - var links = PagingHelper.GetLinks(new Uri("http://localhost.com/api/events"), 32, 2, 10); + var links = PagingHelper.GetLinks(new Uri("http://localhost.com/api/events"), 32, 2, 10); - var result = links.Split(','); - Assert.Equal(4, result.Length); - Assert.Equal("; rel=\"first\"", result[0]); - Assert.Equal("; rel=\"prev\"", result[1]); - Assert.Equal("; rel=\"next\"", result[2]); - Assert.Equal("; rel=\"last\"", result[3]); - } + var result = links.Split(','); + Assert.Equal(4, result.Length); + Assert.Equal("; rel=\"first\"", result[0]); + Assert.Equal("; rel=\"prev\"", result[1]); + Assert.Equal("; rel=\"next\"", result[2]); + Assert.Equal("; rel=\"last\"", result[3]); + } - [Fact] - public void ShouldProperlyReturnLinksPage1Of4() - { - var links = PagingHelper.GetLinks(new Uri("http://localhost.com/api/events"), 32, 1, 10); + [Fact] + public void ShouldProperlyReturnLinksPage1Of4() + { + var links = PagingHelper.GetLinks(new Uri("http://localhost.com/api/events"), 32, 1, 10); - var result = links.Split(','); - Assert.Equal(2, result.Length); - Assert.Equal("; rel=\"next\"", result[0]); - Assert.Equal("; rel=\"last\"", result[1]); - } + var result = links.Split(','); + Assert.Equal(2, result.Length); + Assert.Equal("; rel=\"next\"", result[0]); + Assert.Equal("; rel=\"last\"", result[1]); + } - [Fact] - public void ShouldProperlyReturnLinksPage4Of4() - { - var links = PagingHelper.GetLinks(new Uri("http://localhost.com/api/events"), 32, 4, 10); + [Fact] + public void ShouldProperlyReturnLinksPage4Of4() + { + var links = PagingHelper.GetLinks(new Uri("http://localhost.com/api/events"), 32, 4, 10); - var result = links.Split(','); - Assert.Equal(2, result.Length); - Assert.Equal("; rel=\"first\"", result[0]); - Assert.Equal("; rel=\"prev\"", result[1]); - } + var result = links.Split(','); + Assert.Equal(2, result.Length); + Assert.Equal("; rel=\"first\"", result[0]); + Assert.Equal("; rel=\"prev\"", result[1]); + } - [Fact] - public void ShouldProperlyReturnLinksPage3Of4() - { - var links = PagingHelper.GetLinks(new Uri("http://localhost.com/api/events"), 32, 3, 10); + [Fact] + public void ShouldProperlyReturnLinksPage3Of4() + { + var links = PagingHelper.GetLinks(new Uri("http://localhost.com/api/events"), 32, 3, 10); - var result = links.Split(','); - Assert.Equal(4, result.Length); - Assert.Equal("; rel=\"first\"", result[0]); - Assert.Equal("; rel=\"prev\"", result[1]); - Assert.Equal("; rel=\"next\"", result[2]); - Assert.Equal("; rel=\"last\"", result[3]); - } + var result = links.Split(','); + Assert.Equal(4, result.Length); + Assert.Equal("; rel=\"first\"", result[0]); + Assert.Equal("; rel=\"prev\"", result[1]); + Assert.Equal("; rel=\"next\"", result[2]); + Assert.Equal("; rel=\"last\"", result[3]); + } - [Fact] - public void ShouldProperlyReturnLinksPage1Of1() - { - var links = PagingHelper.GetLinks(new Uri("http://localhost.com/api/events"), 30, 1, 30); + [Fact] + public void ShouldProperlyReturnLinksPage1Of1() + { + var links = PagingHelper.GetLinks(new Uri("http://localhost.com/api/events"), 30, 1, 30); - var result = links.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - Assert.Empty(result); - } + var result = links.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + Assert.Empty(result); + } - [Fact] - public void ShouldProperlyReturnLinksPage1Of4WithCurrentPage0() - { - var links = PagingHelper.GetLinks(new Uri("http://localhost.com/api/events"), 32, 0, 10); + [Fact] + public void ShouldProperlyReturnLinksPage1Of4WithCurrentPage0() + { + var links = PagingHelper.GetLinks(new Uri("http://localhost.com/api/events"), 32, 0, 10); - var result = links.Split(','); - Assert.Equal(2, result.Length); - Assert.Equal("; rel=\"next\"", result[0]); - Assert.Equal("; rel=\"last\"", result[1]); - } + var result = links.Split(','); + Assert.Equal(2, result.Length); + Assert.Equal("; rel=\"next\"", result[0]); + Assert.Equal("; rel=\"last\"", result[1]); + } - [Fact] - public void ShouldProperlyReturnLinksWithExistingQuery() - { - var links = PagingHelper.GetLinks(new Uri("http://localhost.com/api/events?sort=Name&search=тест"), 32, 3, 10); + [Fact] + public void ShouldProperlyReturnLinksWithExistingQuery() + { + var links = PagingHelper.GetLinks(new Uri("http://localhost.com/api/events?sort=Name&search=тест"), 32, 3, 10); - var result = links.Split(','); - Assert.Equal(4, result.Length); - Assert.Equal("; rel=\"first\"", result[0]); - Assert.Equal("; rel=\"prev\"", result[1]); - Assert.Equal("; rel=\"next\"", result[2]); - Assert.Equal("; rel=\"last\"", result[3]); - } + var result = links.Split(','); + Assert.Equal(4, result.Length); + Assert.Equal("; rel=\"first\"", result[0]); + Assert.Equal("; rel=\"prev\"", result[1]); + Assert.Equal("; rel=\"next\"", result[2]); + Assert.Equal("; rel=\"last\"", result[3]); } } diff --git a/src/Monq.Core.Paging.Tests/PagingResponseHeadersTests.cs b/src/Monq.Core.Paging.Tests/PagingResponseHeadersTests.cs index faaba70..3d31a43 100644 --- a/src/Monq.Core.Paging.Tests/PagingResponseHeadersTests.cs +++ b/src/Monq.Core.Paging.Tests/PagingResponseHeadersTests.cs @@ -4,51 +4,50 @@ using System.Linq; using Xunit; -namespace Monq.Core.Paging.Tests +namespace Monq.Core.Paging.Tests; + +public class PagingResponseHeadersTests { - public class PagingResponseHeadersTests + class Build { - class Build - { - public int Id { get; set; } - } + public int Id { get; set; } + } - IEnumerable GenerateBuilds(int count = 1) + IEnumerable GenerateBuilds(int count = 1) + { + var builds = new List(); + for (var i = 0; i < count; i++) { - var builds = new List(); - for (var i = 0; i < count; i++) - { - builds.Add(new Build { Id = i }); - } - - return builds; + builds.Add(new Build { Id = i }); } - [Fact(DisplayName = "Проверка корректной обработки заголовков HTTP-ответа.")] - public void ShouldProperlyParsePagingResponseHeaders() - { - var paging = new Models.PagingModel { Page = 1, PerPage = 4 }; + return builds; + } - var httpContext = new DefaultHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("localhost", 5005); - httpContext.Request.Path = "/api/builds/byjobids"; - httpContext.Request.QueryString = new QueryString("?jobs=1,3"); + [Fact(DisplayName = "Проверка корректной обработки заголовков HTTP-ответа.")] + public void ShouldProperlyParsePagingResponseHeaders() + { + var paging = new Models.PagingModel { Page = 1, PerPage = 4 }; - var builds = GenerateBuilds(8); - var result = builds - .AsQueryable() - .WithPaging(paging, httpContext, x => x.Id); + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("localhost", 5005); + httpContext.Request.Path = "/api/builds/byjobids"; + httpContext.Request.QueryString = new QueryString("?jobs=1,3"); - Assert.Equal(4, result.Count()); + var builds = GenerateBuilds(8); + var result = builds + .AsQueryable() + .WithPaging(paging, httpContext, x => x.Id); - var pagingData = httpContext.Response.GetPagingData(); + Assert.Equal(4, result.Count()); - Assert.Equal(8, pagingData.TotalRecords); - Assert.Equal(8, pagingData.TotalFilteredRecords); - Assert.Equal(4, pagingData.PerPage); - Assert.Equal(1, pagingData.Page); - Assert.Equal(2, pagingData.TotalPages); - } + var pagingData = httpContext.Response.GetPagingData(); + + Assert.Equal(8, pagingData.TotalRecords); + Assert.Equal(8, pagingData.TotalFilteredRecords); + Assert.Equal(4, pagingData.PerPage); + Assert.Equal(1, pagingData.Page); + Assert.Equal(2, pagingData.TotalPages); } } diff --git a/src/Monq.Core.Paging.Tests/TestHelpers.cs b/src/Monq.Core.Paging.Tests/TestHelpers.cs index 1bd3373..8b42717 100644 --- a/src/Monq.Core.Paging.Tests/TestHelpers.cs +++ b/src/Monq.Core.Paging.Tests/TestHelpers.cs @@ -5,53 +5,52 @@ using System.Threading; using System.Threading.Tasks; -namespace Monq.Core.Paging.Tests +namespace Monq.Core.Paging.Tests; + +class TestAsyncEnumerable : EnumerableQuery, IAsyncEnumerable, IQueryable { - class TestAsyncEnumerable : EnumerableQuery, IAsyncEnumerable, IQueryable + public TestAsyncEnumerable(IEnumerable enumerable) : base(enumerable) { - public TestAsyncEnumerable(IEnumerable enumerable) : base(enumerable) - { - } + } - public TestAsyncEnumerable(Expression expression) : base(expression) - { - } + public TestAsyncEnumerable(Expression expression) : base(expression) + { + } - IQueryProvider IQueryable.Provider => new TestAsyncQueryProvider(this); + IQueryProvider IQueryable.Provider => new TestAsyncQueryProvider(this); - public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = new CancellationToken()) => - new TestAsyncEnumerator(this.AsEnumerable().GetEnumerator()); - } + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = new CancellationToken()) => + new TestAsyncEnumerator(this.AsEnumerable().GetEnumerator()); +} - class TestAsyncQueryProvider : IAsyncQueryProvider - { - readonly IQueryProvider _inner; +class TestAsyncQueryProvider : IAsyncQueryProvider +{ + readonly IQueryProvider _inner; - public TestAsyncQueryProvider(IQueryProvider inner) => _inner = inner; + public TestAsyncQueryProvider(IQueryProvider inner) => _inner = inner; - public IQueryable CreateQuery(Expression expression) => new TestAsyncEnumerable(expression); + public IQueryable CreateQuery(Expression expression) => new TestAsyncEnumerable(expression); - public IQueryable CreateQuery(Expression expression) => new TestAsyncEnumerable(expression); + public IQueryable CreateQuery(Expression expression) => new TestAsyncEnumerable(expression); - public object Execute(Expression expression) => _inner.Execute(expression); + public object Execute(Expression expression) => _inner.Execute(expression); - public TResult Execute(Expression expression) => _inner.Execute(expression); + public TResult Execute(Expression expression) => _inner.Execute(expression); - public TResult ExecuteAsync(Expression expression, CancellationToken cancellationToken = new CancellationToken()) => Execute(expression); - } + public TResult ExecuteAsync(Expression expression, CancellationToken cancellationToken = new CancellationToken()) => Execute(expression); +} - class TestAsyncEnumerator : IAsyncEnumerator - { - readonly IEnumerator _inner; +class TestAsyncEnumerator : IAsyncEnumerator +{ + readonly IEnumerator _inner; - public TestAsyncEnumerator(IEnumerator inner) => _inner = inner; + public TestAsyncEnumerator(IEnumerator inner) => _inner = inner; #pragma warning disable 1998 - public async ValueTask DisposeAsync() => _inner.Dispose(); + public async ValueTask DisposeAsync() => _inner.Dispose(); - public async ValueTask MoveNextAsync() => _inner.MoveNext(); + public async ValueTask MoveNextAsync() => _inner.MoveNext(); #pragma warning restore 1998 - public T Current => _inner.Current; - } + public T Current => _inner.Current; } diff --git a/src/Monq.Core.Paging/Constants.cs b/src/Monq.Core.Paging/Constants.cs index 35ae4bf..61a5745 100644 --- a/src/Monq.Core.Paging/Constants.cs +++ b/src/Monq.Core.Paging/Constants.cs @@ -1,16 +1,15 @@ -namespace Monq.Core.Paging +namespace Monq.Core.Paging; + +public static class Constants { - public static class Constants + public static class Headers { - public static class Headers - { - public const string Link = "Link"; + public const string Link = "Link"; - public const string TotalRecords = "X-Total"; - public const string TotalFilteredRecords = "X-Total-Filtered"; - public const string PerPage = "X-Per-Page"; - public const string Page = "X-Page"; - public const string TotalPages = "X-Total-Pages"; - } + public const string TotalRecords = "X-Total"; + public const string TotalFilteredRecords = "X-Total-Filtered"; + public const string PerPage = "X-Per-Page"; + public const string Page = "X-Page"; + public const string TotalPages = "X-Total-Pages"; } } diff --git a/src/Monq.Core.Paging/Extensions/ExpressionHelperExtensions.cs b/src/Monq.Core.Paging/Extensions/ExpressionHelperExtensions.cs index 73b5f0b..d1ffdbe 100644 --- a/src/Monq.Core.Paging/Extensions/ExpressionHelperExtensions.cs +++ b/src/Monq.Core.Paging/Extensions/ExpressionHelperExtensions.cs @@ -6,285 +6,284 @@ using System.Linq.Expressions; using System.Reflection; -namespace Monq.Core.Paging.Extensions +namespace Monq.Core.Paging.Extensions; + +static class ExpressionHelperExtensions { - static class ExpressionHelperExtensions + static readonly MethodInfo _orderByAscMethod = + typeof(Queryable).GetMethods() + .First(method => method.Name == nameof(Queryable.OrderBy) + && method.GetParameters().Length == 2); + + static readonly MethodInfo _orderByDescMethod = + typeof(Queryable).GetMethods() + .First(method => method.Name == nameof(Queryable.OrderByDescending) + && method.GetParameters().Length == 2); + + /// + /// Отсортировать по возрастанию. + /// + /// The type of the source. + /// The source. + /// The lambda. + public static IQueryable OrderBy(this IQueryable source, LambdaExpression lambda) + => _orderByAscMethod.Call>(new[] { typeof(TSource), lambda.ReturnType }, source, lambda); + + /// + /// Отсортировать по убыванию. + /// + /// The type of the source. + /// The source. + /// The lambda. + public static IQueryable OrderByDescending(this IQueryable source, LambdaExpression lambda) + => _orderByDescMethod.Call>(new[] { typeof(TSource), lambda.ReturnType }, source, lambda); + + /// + /// Вызвать метод с указанными параметрами. + /// + /// The type of the result. + /// The method. + /// The type arguments. + /// The arguments. + public static TResult Call(this MethodInfo method, Type[] typeArgs, params object[] args) { - static readonly MethodInfo _orderByAscMethod = - typeof(Queryable).GetMethods() - .First(method => method.Name == nameof(Queryable.OrderBy) - && method.GetParameters().Length == 2); - - static readonly MethodInfo _orderByDescMethod = - typeof(Queryable).GetMethods() - .First(method => method.Name == nameof(Queryable.OrderByDescending) - && method.GetParameters().Length == 2); - - /// - /// Отсортировать по возрастанию. - /// - /// The type of the source. - /// The source. - /// The lambda. - public static IQueryable OrderBy(this IQueryable source, LambdaExpression lambda) - => _orderByAscMethod.Call>(new[] { typeof(TSource), lambda.ReturnType }, source, lambda); - - /// - /// Отсортировать по убыванию. - /// - /// The type of the source. - /// The source. - /// The lambda. - public static IQueryable OrderByDescending(this IQueryable source, LambdaExpression lambda) - => _orderByDescMethod.Call>(new[] { typeof(TSource), lambda.ReturnType }, source, lambda); - - /// - /// Вызвать метод с указанными параметрами. - /// - /// The type of the result. - /// The method. - /// The type arguments. - /// The arguments. - public static TResult Call(this MethodInfo method, Type[] typeArgs, params object[] args) - { - if (method.IsStatic) - return (TResult)method.MakeGenericMethod(typeArgs).Invoke(null, args); + if (method.IsStatic) + return (TResult)method.MakeGenericMethod(typeArgs).Invoke(null, args); - return (TResult)method.MakeGenericMethod(typeArgs).Invoke(args.FirstOrDefault(), args.Skip(1).ToArray()); - } + return (TResult)method.MakeGenericMethod(typeArgs).Invoke(args.FirstOrDefault(), args.Skip(1).ToArray()); + } - /// - /// Получить свойство по имени. - /// - /// The type. - /// The name. - /// if set to true [Игнорировать регистр в имени свойства]. - public static PropertyInfo? GetProperty(this Type type, string name, bool ignoreCase) - => type.GetProperties().FirstOrDefault(x => string.Equals(x.Name, name, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)); - - /// - /// Получить тип свойства. - /// - /// The type. - /// The path. - public static Type? GetPropertyType(this Type? type, string path) - => path.Split('.', StringSplitOptions.RemoveEmptyEntries) - .Aggregate(type, - (propType, name) => propType is not null && propType.IsGenericType - ? propType.GetGenericArguments()[0].GetProperty(name)?.PropertyType - : propType?.GetProperty(name)?.PropertyType); - - /// - /// Получить выражение получения свойства. - /// - /// The expression. - /// The path. - public static Expression? GetPropertyExpression(this Expression expression, string path) - => expression.Type.GetProperties(path) - .Aggregate(expression, Expression.Property) - .AddNullConditions(); - - /// - /// Получить выражение получения свойства (без проверок на null). - /// - /// The expression. - /// The path. - public static Expression GetPropertyExpressionUnSafe(this Expression expression, string path) - => expression.Type.GetProperties(path) - .Aggregate(expression, Expression.Property); - - /// - /// Получить значение типа по умолчанию. - /// - /// Выражение. - public static object? GetDefault(this Type type) - => type.IsValueType ? Activator.CreateInstance(type) : null; - - /// - /// Получить выражение константу со значением типа по умолчанию. - /// - /// Выражение. - public static Expression GetDefaultConstantExpr(this Expression? expr) - => Expression.Constant(expr.Type.GetDefault(), expr.Type); - - /// - /// Создать выражения на проверку на null. - /// - /// The expr. - /// The value. - /// The default value. - public static Expression AddNullCondition(this Expression expr, Expression val, Expression defaultValue) - => Expression.Condition(Expression.Equal(val, Expression.Constant(null, val.Type)), defaultValue, expr); - - /// - /// Добавить в дерево выражения проверки на null (?.). - /// - /// Выражение. - /// Значение по умолчанию. - public static Expression? AddNullConditions(this Expression? expr, Expression defaultValue) - { - var safe = expr; + /// + /// Получить свойство по имени. + /// + /// The type. + /// The name. + /// if set to true [Игнорировать регистр в имени свойства]. + public static PropertyInfo? GetProperty(this Type type, string name, bool ignoreCase) + => type.GetProperties().FirstOrDefault(x => string.Equals(x.Name, name, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)); + + /// + /// Получить тип свойства. + /// + /// The type. + /// The path. + public static Type? GetPropertyType(this Type? type, string path) + => path.Split('.', StringSplitOptions.RemoveEmptyEntries) + .Aggregate(type, + (propType, name) => propType is not null && propType.IsGenericType + ? propType.GetGenericArguments()[0].GetProperty(name)?.PropertyType + : propType?.GetProperty(name)?.PropertyType); + + /// + /// Получить выражение получения свойства. + /// + /// The expression. + /// The path. + public static Expression? GetPropertyExpression(this Expression expression, string path) + => expression.Type.GetProperties(path) + .Aggregate(expression, Expression.Property) + .AddNullConditions(); + + /// + /// Получить выражение получения свойства (без проверок на null). + /// + /// The expression. + /// The path. + public static Expression GetPropertyExpressionUnSafe(this Expression expression, string path) + => expression.Type.GetProperties(path) + .Aggregate(expression, Expression.Property); + + /// + /// Получить значение типа по умолчанию. + /// + /// Выражение. + public static object? GetDefault(this Type type) + => type.IsValueType ? Activator.CreateInstance(type) : null; + + /// + /// Получить выражение константу со значением типа по умолчанию. + /// + /// Выражение. + public static Expression GetDefaultConstantExpr(this Expression? expr) + => Expression.Constant(expr.Type.GetDefault(), expr.Type); + + /// + /// Создать выражения на проверку на null. + /// + /// The expr. + /// The value. + /// The default value. + public static Expression AddNullCondition(this Expression expr, Expression val, Expression defaultValue) + => Expression.Condition(Expression.Equal(val, Expression.Constant(null, val.Type)), defaultValue, expr); + + /// + /// Добавить в дерево выражения проверки на null (?.). + /// + /// Выражение. + /// Значение по умолчанию. + public static Expression? AddNullConditions(this Expression? expr, Expression defaultValue) + { + var safe = expr; - while (!IsNullSafe(expr, out var obj)) - { - safe = safe.AddNullCondition(obj, defaultValue); - expr = obj; - } - return safe; + while (!IsNullSafe(expr, out var obj)) + { + safe = safe.AddNullCondition(obj, defaultValue); + expr = obj; } + return safe; + } - /// - /// Добавить в дерево выражения проверки на null (?.). - /// - /// The expr. - public static Expression? AddNullConditions(this Expression? expr) => - expr.AddNullConditions(expr.GetDefaultConstantExpr()); - - /// - /// Получить для указанного типа цепочку PropertyInfo по заданному полному пути. - /// - /// Тип объекта. - /// Полный путь свойства. - /// if set to true [ignore case]. - public static IEnumerable GetProperties(this Type type, string? path, bool ignoreCase = false) + /// + /// Добавить в дерево выражения проверки на null (?.). + /// + /// The expr. + public static Expression? AddNullConditions(this Expression? expr) => + expr.AddNullConditions(expr.GetDefaultConstantExpr()); + + /// + /// Получить для указанного типа цепочку PropertyInfo по заданному полному пути. + /// + /// Тип объекта. + /// Полный путь свойства. + /// if set to true [ignore case]. + public static IEnumerable GetProperties(this Type type, string? path, bool ignoreCase = false) + { + if (string.IsNullOrEmpty(path)) + yield break; + foreach (var propName in path.Trim().Split('.', StringSplitOptions.RemoveEmptyEntries)) { - if (string.IsNullOrEmpty(path)) + var prop = type.GetProperty(propName, ignoreCase); + if (prop is null) yield break; - foreach (var propName in path.Trim().Split('.', StringSplitOptions.RemoveEmptyEntries)) - { - var prop = type.GetProperty(propName, ignoreCase); - if (prop is null) - yield break; - yield return prop; - type = prop.PropertyType; - } + yield return prop; + type = prop.PropertyType; } + } - /// - /// Проверить правильность полного пути свойства, с возвращение его с учетом правильного регистра. - /// - /// Тип. - /// Полный путь. - public static string GetValidPropertyName(this Type type, string? path) - => string.Join(".", type.GetProperties(path, true).Select(x => x.Name)); + /// + /// Проверить правильность полного пути свойства, с возвращение его с учетом правильного регистра. + /// + /// Тип. + /// Полный путь. + public static string GetValidPropertyName(this Type type, string? path) + => string.Join(".", type.GetProperties(path, true).Select(x => x.Name)); - static bool IsNullSafe(Expression? expr, out Expression? nullableObject) - { - nullableObject = null; + static bool IsNullSafe(Expression? expr, out Expression? nullableObject) + { + nullableObject = null; - if (expr is not MemberExpression && expr is not MethodCallExpression) - return true; + if (expr is not MemberExpression && expr is not MethodCallExpression) + return true; - Expression? obj; - var callExpr = expr as MethodCallExpression; + Expression? obj; + var callExpr = expr as MethodCallExpression; - if (expr is MemberExpression memberExpr) - { - // Static fields don't require an instance - var field = memberExpr.Member as FieldInfo; - if (field != null && field.IsStatic) - return true; + if (expr is MemberExpression memberExpr) + { + // Static fields don't require an instance + var field = memberExpr.Member as FieldInfo; + if (field != null && field.IsStatic) + return true; - // Static properties don't require an instance - var property = memberExpr.Member as PropertyInfo; - if (property != null) - { - var getter = property.GetGetMethod(); - if (getter != null && getter.IsStatic) - return true; - } - obj = memberExpr.Expression; - } - else + // Static properties don't require an instance + var property = memberExpr.Member as PropertyInfo; + if (property != null) { - // Static methods don't require an instance - if (callExpr.Method.IsStatic) + var getter = property.GetGetMethod(); + if (getter != null && getter.IsStatic) return true; - - obj = callExpr.Object; } - - // Value types can't be null - // Проверки на Null требуется проводить только по вложенным свойствам. - if (obj.Type.IsValueType || obj.NodeType == ExpressionType.Parameter) + obj = memberExpr.Expression; + } + else + { + // Static methods don't require an instance + if (callExpr.Method.IsStatic) return true; - // Instance member access or instance method call is not safe - nullableObject = obj; - return false; + obj = callExpr.Object; } - /// - /// Декомпилировать свойства помеченные атрибутом Computed. - /// - /// The expr. - public static Expression Decompile(this Expression expr) - => DecompileExpressionVisitor.Decompile(expr); - - /// - /// Превратить вызовы в константы (на данный момент поддерживается только вызовы над DateTimeOffsets). - /// - /// The expr. - public static Expression ExpressionCallsToConstants(this Expression expr) - => ExpressionConstantCallVisitor.ExpressionCallsToConstants(expr); - - /// - /// Получить полное имя свойства. - /// - /// Тип объекта, которому принадлежит свойство - /// Тип свойства. - /// The expression. - public static string GetFullPropertyName(this Expression> expression) - { - var props = new List(); - var member = GetMemberExpression(expression); - while (member != null) - { - props.Add(member.Member.Name); - member = GetMemberExpression(member.Expression); - } - props.Reverse(); - return string.Join('.', props); - } + // Value types can't be null + // Проверки на Null требуется проводить только по вложенным свойствам. + if (obj.Type.IsValueType || obj.NodeType == ExpressionType.Parameter) + return true; - /// - /// Получить публичные свойства типа, включая вложенные свойства на указанной глубине - /// и полное имя свойства удовлетворяющее заданному выражению. - /// - /// Тип. - /// Глубина. - /// The exp. - public static IEnumerable<(string FullName, PropertyInfo Property)> GetPublicProperties(this Type type, int depth = 1, Func? exp = null) + // Instance member access or instance method call is not safe + nullableObject = obj; + return false; + } + + /// + /// Декомпилировать свойства помеченные атрибутом Computed. + /// + /// The expr. + public static Expression Decompile(this Expression expr) + => DecompileExpressionVisitor.Decompile(expr); + + /// + /// Превратить вызовы в константы (на данный момент поддерживается только вызовы над DateTimeOffsets). + /// + /// The expr. + public static Expression ExpressionCallsToConstants(this Expression expr) + => ExpressionConstantCallVisitor.ExpressionCallsToConstants(expr); + + /// + /// Получить полное имя свойства. + /// + /// Тип объекта, которому принадлежит свойство + /// Тип свойства. + /// The expression. + public static string GetFullPropertyName(this Expression> expression) + { + var props = new List(); + var member = GetMemberExpression(expression); + while (member != null) { - exp ??= (_) => true; + props.Add(member.Member.Name); + member = GetMemberExpression(member.Expression); + } + props.Reverse(); + return string.Join('.', props); + } - var properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public) - .Where(x => exp(x.Name)) - .Select(x => (x.Name, Property: x)) - .ToList(); - if (depth <= 1) return properties; + /// + /// Получить публичные свойства типа, включая вложенные свойства на указанной глубине + /// и полное имя свойства удовлетворяющее заданному выражению. + /// + /// Тип. + /// Глубина. + /// The exp. + public static IEnumerable<(string FullName, PropertyInfo Property)> GetPublicProperties(this Type type, int depth = 1, Func? exp = null) + { + exp ??= (_) => true; - var nextLevel = properties - .Where(x => x.Property.PropertyType.IsClass && x.Property.PropertyType != typeof(string)) - .SelectMany(x => GetPublicProperties(x.Property.PropertyType, depth - 1) - .Select(y => (FullName: $"{x.Property.Name}.{y.FullName}", y.Property))) - .Where(x => exp(x.FullName)); + var properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(x => exp(x.Name)) + .Select(x => (x.Name, Property: x)) + .ToList(); + if (depth <= 1) return properties; - return properties.Concat(nextLevel); - } + var nextLevel = properties + .Where(x => x.Property.PropertyType.IsClass && x.Property.PropertyType != typeof(string)) + .SelectMany(x => GetPublicProperties(x.Property.PropertyType, depth - 1) + .Select(y => (FullName: $"{x.Property.Name}.{y.FullName}", y.Property))) + .Where(x => exp(x.FullName)); - /// - /// Получить MemberExpression выражения. - /// - /// Выражение. - public static MemberExpression? GetMemberExpression(this Expression? expression) => - expression switch - { - MemberExpression memberExpression => memberExpression, - LambdaExpression lambdaExpression when lambdaExpression.Body is MemberExpression body => body, - LambdaExpression lambdaExpression when lambdaExpression.Body is UnaryExpression unaryExpression => - (MemberExpression)unaryExpression.Operand, - _ => null - }; + return properties.Concat(nextLevel); } + + /// + /// Получить MemberExpression выражения. + /// + /// Выражение. + public static MemberExpression? GetMemberExpression(this Expression? expression) => + expression switch + { + MemberExpression memberExpression => memberExpression, + LambdaExpression lambdaExpression when lambdaExpression.Body is MemberExpression body => body, + LambdaExpression lambdaExpression when lambdaExpression.Body is UnaryExpression unaryExpression => + (MemberExpression)unaryExpression.Operand, + _ => null + }; } \ No newline at end of file diff --git a/src/Monq.Core.Paging/Extensions/HttpExtensions.cs b/src/Monq.Core.Paging/Extensions/HttpExtensions.cs index 702af82..a88a735 100644 --- a/src/Monq.Core.Paging/Extensions/HttpExtensions.cs +++ b/src/Monq.Core.Paging/Extensions/HttpExtensions.cs @@ -1,84 +1,83 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Monq.Core.Paging.Models; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using static Monq.Core.Paging.Constants.Headers; -namespace Monq.Core.Paging.Extensions +namespace Monq.Core.Paging.Extensions; + +/// +/// Методы расширения для получения данных постраничной навигации из +/// или . +/// +public static class HttpExtensions { /// - /// Методы расширения для получения данных постраничной навигации из - /// или . + /// Получить модель постраничного представления . из заголовков HTTP-ответа + /// . /// - public static class HttpExtensions + /// HTTP-ответ с заголовками постраничной навигации. + /// + /// Модель постраничного представления . + /// + public static PagingResponseHeaders GetPagingData(this HttpResponse? httpResponse) { - /// - /// Получить модель постраничного представления . из заголовков HTTP-ответа - /// . - /// - /// HTTP-ответ с заголовками постраничной навигации. - /// - /// Модель постраничного представления . - /// - public static PagingResponseHeaders GetPagingData(this HttpResponse? httpResponse) - { - if (httpResponse?.Headers is null) - return new PagingResponseHeaders(); + if (httpResponse?.Headers is null) + return new PagingResponseHeaders(); - var headers = httpResponse.Headers; + var headers = httpResponse.Headers; - var totalRecords = headers.TryGetValue(TotalRecords); - var totalFilteredRecords = headers.TryGetValue(TotalFilteredRecords); - var perPage = headers.TryGetValue(PerPage); - var page = headers.TryGetValue(Page); - var totalPages = headers.TryGetValue(TotalPages); + var totalRecords = headers.TryGetValue(TotalRecords); + var totalFilteredRecords = headers.TryGetValue(TotalFilteredRecords); + var perPage = headers.TryGetValue(PerPage); + var page = headers.TryGetValue(Page); + var totalPages = headers.TryGetValue(TotalPages); - var result = new PagingResponseHeaders(totalRecords, totalFilteredRecords, perPage, page, totalPages); - return result; - } + var result = new PagingResponseHeaders(totalRecords, totalFilteredRecords, perPage, page, totalPages); + return result; + } - /// - /// Получить модель постраничного представления . из заголовков HTTP-ответа - /// . - /// - /// HTTP-ответ с заголовками постраничной навигации. - /// - /// Модель постраничного представления . - /// - public static PagingResponseHeaders GetPagingData(this HttpResponseMessage? httpResponseMessage) - { - if (httpResponseMessage?.Headers is null) - return new PagingResponseHeaders(); + /// + /// Получить модель постраничного представления . из заголовков HTTP-ответа + /// . + /// + /// HTTP-ответ с заголовками постраничной навигации. + /// + /// Модель постраничного представления . + /// + public static PagingResponseHeaders GetPagingData(this HttpResponseMessage? httpResponseMessage) + { + if (httpResponseMessage?.Headers is null) + return new PagingResponseHeaders(); - var headers = httpResponseMessage.Headers; + var headers = httpResponseMessage.Headers; - var totalRecords = headers.TryGetValue(TotalRecords); - var totalFilteredRecords = headers.TryGetValue(TotalFilteredRecords); - var perPage = headers.TryGetValue(PerPage); - var page = headers.TryGetValue(Page); - var totalPages = headers.TryGetValue(TotalPages); + var totalRecords = headers.TryGetValue(TotalRecords); + var totalFilteredRecords = headers.TryGetValue(TotalFilteredRecords); + var perPage = headers.TryGetValue(PerPage); + var page = headers.TryGetValue(Page); + var totalPages = headers.TryGetValue(TotalPages); - var result = new PagingResponseHeaders(totalRecords, totalFilteredRecords, perPage, page, totalPages); - return result; - } + var result = new PagingResponseHeaders(totalRecords, totalFilteredRecords, perPage, page, totalPages); + return result; + } - static int TryGetValue(this HttpHeaders headers, string key) - { - if (!headers.TryGetValues(key, out var values)) - return 0; + static int TryGetValue(this HttpHeaders headers, string key) + { + if (!headers.TryGetValues(key, out var values)) + return 0; - var value = values.FirstOrDefault() ?? string.Empty; - return !int.TryParse(value, out var result) ? 0 : result; - } + var value = values.FirstOrDefault() ?? string.Empty; + return !int.TryParse(value, out var result) ? 0 : result; + } - static int TryGetValue(this IHeaderDictionary headers, string key) - { - if (!headers.ContainsKey(key)) - return 0; + static int TryGetValue(this IHeaderDictionary headers, string key) + { + if (!headers.ContainsKey(key)) + return 0; - var value = headers[key].FirstOrDefault() ?? string.Empty; - return !int.TryParse(value, out var result) ? 0 : result; - } + var value = headers[key].FirstOrDefault() ?? string.Empty; + return !int.TryParse(value, out var result) ? 0 : result; } } diff --git a/src/Monq.Core.Paging/Extensions/PagingExtensions.cs b/src/Monq.Core.Paging/Extensions/PagingExtensions.cs index 5ae35cf..9e287f7 100644 --- a/src/Monq.Core.Paging/Extensions/PagingExtensions.cs +++ b/src/Monq.Core.Paging/Extensions/PagingExtensions.cs @@ -1,407 +1,257 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Http; using Monq.Core.Paging.Helpers; using Monq.Core.Paging.Models; -using Monq.Core.Paging.Models.DataTable; using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Net.Http; -using System.Threading.Tasks; -namespace Monq.Core.Paging.Extensions +namespace Monq.Core.Paging.Extensions; + +/// +/// Расширения для постраничного вывода данных. +/// +public static class PagingExtensions { + struct PagingResult + { + public int TotalItemsCount; + public int ItemsFilteredCount; + public int CurrentPage; + public int TotalPages; + } + /// - /// Расширения для постраничного вывода данных. + /// Получить постраничное представление с мета данными в HTTP Header. /// - public static class PagingExtensions + /// Тип модели. + /// Тип ключа сортировки. + /// Модель. + /// Фильтр постраничной навигации. + /// The HTTP context. + /// Сортировка по умолчанию. + /// Выражение для поиска. + /// Ссылка, на базе которой будут сформированы заголовки Http Header Link. Если link == null, то будет использован . + /// defaultOrder - defaultOrder + public static IQueryable WithPaging( + this IQueryable data, + PagingModel paging, + HttpContext? httpContext, + Expression>? defaultOrder, + Expression> searchExpression, + string? link = null) where TSource : class { - struct PagingResult - { - public int TotalItemsCount; - public int ItemsFilteredCount; - public int CurrentPage; - public int TotalPages; - } - - /// - /// Получить постраничное представление с мета данными в HTTP Header. - /// - /// Тип модели. - /// Тип ключа сортировки. - /// Модель. - /// Фильтр постраничной навигации. - /// The HTTP context. - /// Сортировка по умолчанию. - /// Выражение для поиска. - /// Ссылка, на базе которой будут сформированы заголовки Http Header Link. Если link == null, то будет использован . - /// defaultOrder - defaultOrder - public static IQueryable WithPaging( - this IQueryable data, - PagingModel paging, - HttpContext? httpContext, - Expression>? defaultOrder, - Expression> searchExpression, - string? link = null) where TSource : class - { - if (defaultOrder is null) - throw new ArgumentNullException(nameof(defaultOrder), $"{nameof(defaultOrder)} is null."); - - return data.GetPaging(paging, httpContext, defaultOrder, link, searchExpression); - } + if (defaultOrder is null) + throw new ArgumentNullException(nameof(defaultOrder), $"{nameof(defaultOrder)} is null."); - /// - /// Получить постраничное представление с мета данными в . - /// - /// Тип данных. - /// Тип ключа сортировки. - /// Набор данных типа . - /// Фильтр постраничной навигации. - /// The HTTP context. - /// Сортировка по умолчанию. - /// Ссылка, на базе которой будут сформированы заголовки Http Header Link. Если link == null, то будет использован . - /// defaultOrder - defaultOrder - public static IQueryable WithPaging( - this IQueryable data, - PagingModel paging, - HttpContext? httpContext, - Expression>? defaultOrder, - string? link = null) where TSource : class - { - if (defaultOrder is null) - throw new ArgumentNullException(nameof(defaultOrder), $"{nameof(defaultOrder)} is null."); + return data.GetPaging(paging, httpContext, defaultOrder, link, searchExpression); + } - return data.GetPaging(paging, httpContext, defaultOrder, link); - } + /// + /// Получить постраничное представление с мета данными в . + /// + /// Тип данных. + /// Тип ключа сортировки. + /// Набор данных типа . + /// Фильтр постраничной навигации. + /// The HTTP context. + /// Сортировка по умолчанию. + /// Ссылка, на базе которой будут сформированы заголовки Http Header Link. Если link == null, то будет использован . + /// defaultOrder - defaultOrder + public static IQueryable WithPaging( + this IQueryable data, + PagingModel paging, + HttpContext? httpContext, + Expression>? defaultOrder, + string? link = null) where TSource : class + { + if (defaultOrder is null) + throw new ArgumentNullException(nameof(defaultOrder), $"{nameof(defaultOrder)} is null."); - /// - /// Получить постраничное представление с мета данными в . - /// - /// Тип данных. - /// Тип ключа сортировки. - /// Набор данных типа . - /// Фильтр постраничной навигации. - /// The HTTP context. - /// Сортировка по умолчанию. - /// Ссылка, на базе которой будут сформированы заголовки Http Header Link. Если link == null, то будет использован . - /// defaultOrder - defaultOrder - public static IQueryable WithPaging( - this IQueryable data, - DataTable paging, - HttpContext? httpContext, - Expression>? defaultOrder, - string? link = null) where TSource : class - { - if (defaultOrder is null) - throw new ArgumentNullException(nameof(defaultOrder), $"{nameof(defaultOrder)} is null."); + return data.GetPaging(paging, httpContext, defaultOrder, link); + } - var searching = new Searching(SearchType.Exclude, paging.Columns?.Where(x => !x.Searchable)?.Select(x => x.Data)); - return data.GetPaging(paging.GetPagingModel(), httpContext, defaultOrder, link, searching); - } + /// + /// Получить постраничное представление с мета данными в . + /// Используйте этот метод на свой страх и риск, т.к. при операциях skip/take должна быть задана сортировка по умолчанию. + /// + /// Тип данных. + /// Набор данных типа . + /// Фильтр постраничной навигации. + /// The HTTP context. + /// Ссылка, на базе которой будут сформированы заголовки Http Header Link. Если link == null, то будет использован . + [Obsolete("Используйте этот метод на свой страх и риск, т.к. при операциях skip/take должна быть задана сортировка по умолчанию.", false)] + public static IQueryable WithPaging( + this IQueryable data, + PagingModel paging, + HttpContext? httpContext, + string? link = null) where TSource : class + { + return data.GetPaging(paging, httpContext, link: link); + } - /// - /// Получить постраничное представление с мета данными в . - /// Используйте этот метод на свой страх и риск, т.к. при операциях skip/take должна быть задана сортировка по умолчанию. - /// - /// Тип данных. - /// Набор данных типа . - /// Фильтр постраничной навигации. - /// The HTTP context. - /// Ссылка, на базе которой будут сформированы заголовки Http Header Link. Если link == null, то будет использован . - [Obsolete("Используйте этот метод на свой страх и риск, т.к. при операциях skip/take должна быть задана сортировка по умолчанию.", false)] - public static IQueryable WithPaging( - this IQueryable data, - PagingModel paging, - HttpContext? httpContext, - string? link = null) where TSource : class + /// + /// Получить постраничное представление с мета данными в HTTP Header. + /// + /// Тип модели. + /// Тип ключа сортировки. + /// Модель. + /// Фильтр постраничной навигации. + /// The HTTP context. + /// Сортировка по умолчанию. + /// Ссылка. + /// Type of the search. + /// Глубина поискового запроса. По умолчанию = 1. + /// Свойства, по которым проводится или не проводится поиск . + /// defaultOrder - defaultOrder + public static IQueryable WithPaging( + this IEnumerable data, + PagingModel paging, + HttpContext? httpContext, + Expression>? defaultOrder, + string? link = null, + SearchType searchType = SearchType.None, + int searchDepth = 1, + params Expression>[] searchProps + ) where TSource : class + { + if (defaultOrder is null) { - return data.GetPaging(paging, httpContext, link: link); + throw new ArgumentNullException(nameof(defaultOrder), $"{nameof(defaultOrder)} is null."); } - /// - /// Получить постраничное представление с мета данными в HTTP Header. - /// - /// Тип модели. - /// Тип ключа сортировки. - /// Модель. - /// Фильтр постраничной навигации. - /// The HTTP context. - /// Сортировка по умолчанию. - /// Ссылка. - /// Type of the search. - /// Глубина поискового запроса. По умолчанию = 1. - /// Свойства, по которым проводится или не проводится поиск . - /// defaultOrder - defaultOrder - public static IQueryable WithPaging( - this IEnumerable data, - PagingModel paging, - HttpContext? httpContext, - Expression>? defaultOrder, - string? link = null, - SearchType searchType = SearchType.None, - int searchDepth = 1, - params Expression>[] searchProps - ) where TSource : class - { - if (defaultOrder is null) - { - throw new ArgumentNullException(nameof(defaultOrder), $"{nameof(defaultOrder)} is null."); - } + return data.AsQueryable().GetPaging(paging, httpContext, defaultOrder, link, Searching.CreateSearching(searchType, searchDepth, searchProps)); + } - return data.AsQueryable().GetPaging(paging, httpContext, defaultOrder, link, Searching.CreateSearching(searchType, searchDepth, searchProps)); - } + static IQueryable GetPaging( + this IQueryable data, + PagingModel paging, + HttpContext? httpContext, + Expression>? defaultOrder = null, + string? link = null, + Searching? searching = null) where TSource : class + { + var searchExpr = searching.GetExpressionForSearch(paging.Search); + return GetPaging(data, paging, httpContext, defaultOrder, link, searchExpr); + } - /// - /// Получить результат постраничной разбивки в представлении . - /// - /// Тип модели. - /// Модель. - /// The HTTP context. - /// - /// - /// - public static DataTablesResponse ToDataTablesResponse( - this IQueryable data, HttpContext httpContext) where TSource : class - { - var pagingData = httpContext.Response.GetPagingData(); - var resultData = data.ToList(); - var result = new DataTablesResponse - { - RecordsTotal = pagingData.TotalRecords, - RecordsFiltered = pagingData.TotalFilteredRecords, - Data = resultData - }; - return result; - } + static IQueryable GetPaging( + this IQueryable data, + PagingModel paging, + HttpContext? httpContext, + Expression>? defaultOrder, + string? link, + Expression>? searchExpression) where TSource : class + { + var sortedAndFilteredData = ApplySortSearchAndPageFilter(data, paging, out var pagingResult, defaultOrder, searchExpression); - /// - /// Получить результат постраничной разбивки в представлении . - /// - /// Тип модели. - /// Модель. - /// The HTTP context. - /// - /// - /// - public static async Task> ToDataTablesResponseAsync( - this IQueryable data, HttpContext httpContext) where TSource : class - { - var pagingData = httpContext.Response.GetPagingData(); - var resultData = await data.ToListAsync(); + if (httpContext is null) + return sortedAndFilteredData; - var result = new DataTablesResponse - { - RecordsTotal = pagingData.TotalRecords, - RecordsFiltered = pagingData.TotalFilteredRecords, - Data = resultData - }; - return result; - } + var itemsTotalCount = pagingResult.TotalItemsCount; + var itemsCount = pagingResult.ItemsFilteredCount; + var currentPage = pagingResult.CurrentPage; + var totalPages = pagingResult.TotalPages; - /// - /// Получить результат постраничной разбивки в представлении . - /// - /// Тип модели. - /// Тип возвращаемой модели. - /// Модель. - /// Map function to map values from to . - /// The HTTP context. - /// - /// - /// - public static DataTablesResponse ToDataTablesResponse( - this IQueryable data, - Func, IEnumerable> mapData, - HttpContext httpContext) where TSource : class + Uri baseUri; + if (link is null) { - var pagingData = httpContext.Response.GetPagingData(); - var resultData = data.ToList(); - var result = new DataTablesResponse + var uriBuilder = new UriBuilder(httpContext.Request.Scheme, httpContext.Request.Host.Host, httpContext.Request.Host.Port ?? 80, httpContext.Request.Path.Value) { - RecordsTotal = pagingData.TotalRecords, - RecordsFiltered = pagingData.TotalFilteredRecords, - Data = mapData(resultData) + Query = httpContext.Request.QueryString.HasValue ? Uri.EscapeUriString(httpContext.Request.QueryString.Value) : string.Empty }; - return result; + baseUri = uriBuilder.Uri; } - - /// - /// Получить результат постраничной разбивки в представлении . - /// - /// Тип модели. - /// Тип возвращаемой модели. - /// Модель. - /// Map function to map values from to . - /// The HTTP context. - /// - /// - /// - public static async Task> ToDataTablesResponseAsync( - this IQueryable data, - Func, IEnumerable> mapData, - HttpContext httpContext) where TSource : class + else { - var pagingData = httpContext.Response.GetPagingData(); - var resultData = await data.ToListAsync(); - var result = new DataTablesResponse - { - RecordsTotal = pagingData.TotalRecords, - RecordsFiltered = pagingData.TotalFilteredRecords, - Data = mapData(resultData) - }; - return result; + baseUri = new Uri(link); } + var links = PagingHelper.GetLinks(baseUri, itemsCount, currentPage, paging.PerPage); - /// - /// Создать модель DataTablesResponse используя указанный response. - /// - /// - /// The elements. - /// The response. - /// The draw. - public static DataTablesResponse CreateDataTablesResponse(this IEnumerable elements, HttpResponseMessage response, in int draw = 0) - { - var pagingMetaData = response.GetPagingData(); - return new DataTablesResponse - { - Data = elements, - Draw = draw, - RecordsTotal = pagingMetaData.TotalRecords, - RecordsFiltered = pagingMetaData.TotalFilteredRecords - }; - } + httpContext.Response.Headers.TryAdd(Constants.Headers.Link, links); + httpContext.Response.Headers.TryAdd(Constants.Headers.TotalRecords, itemsTotalCount.ToString()); + httpContext.Response.Headers.TryAdd(Constants.Headers.TotalFilteredRecords, itemsCount.ToString()); + httpContext.Response.Headers.TryAdd(Constants.Headers.PerPage, paging.PerPage.ToString()); + httpContext.Response.Headers.TryAdd(Constants.Headers.Page, currentPage.ToString()); + httpContext.Response.Headers.TryAdd(Constants.Headers.TotalPages, totalPages.ToString()); + + return sortedAndFilteredData; + } + + static IQueryable ApplySortSearchAndPageFilter( + IQueryable data, + PagingModel paging, + out PagingResult pagingResult, + Expression>? defaultOrder, + Expression>? searchExpression = null) where TSource : class + { + var filteredData = data; + if (!string.IsNullOrWhiteSpace(paging.Search) && searchExpression is not null) + filteredData = data.Where(searchExpression); - static IQueryable GetPaging( - this IQueryable data, - PagingModel paging, - HttpContext? httpContext, - Expression>? defaultOrder = null, - string? link = null, - Searching? searching = null) where TSource : class + IQueryable sortedAndFilteredData; + var sortCol = typeof(TSource).GetValidPropertyName(paging.SortCol); + if (!string.IsNullOrEmpty(sortCol) && sortCol.Length == paging.SortCol.Trim().Length) { - var searchExpr = searching.GetExpressionForSearch(paging.Search); - return GetPaging(data, paging, httpContext, defaultOrder, link, searchExpr); + sortedAndFilteredData = filteredData.OrderByProperty(sortCol, paging.SortDir); } + else if (defaultOrder is not null) + sortedAndFilteredData = filteredData.OrderBy(defaultOrder); + else + sortedAndFilteredData = filteredData; - static IQueryable GetPaging( - this IQueryable data, - PagingModel paging, - HttpContext? httpContext, - Expression>? defaultOrder, - string? link, - Expression>? searchExpression) where TSource : class - { - var sortedAndFilteredData = ApplySortSearchAndPageFilter(data, paging, out var pagingResult, defaultOrder, searchExpression); + var totalItemsCount = data.Count(); + var filteredItemsCount = data == filteredData ? totalItemsCount : sortedAndFilteredData.Count(); - if (httpContext is null) - return sortedAndFilteredData; + sortedAndFilteredData = sortedAndFilteredData.ApplySkipTakeAndGetPagination(paging, filteredItemsCount, out pagingResult); + pagingResult.TotalItemsCount = totalItemsCount; + pagingResult.ItemsFilteredCount = filteredItemsCount; + return sortedAndFilteredData; + } - var itemsTotalCount = pagingResult.TotalItemsCount; - var itemsCount = pagingResult.ItemsFilteredCount; - var currentPage = pagingResult.CurrentPage; - var totalPages = pagingResult.TotalPages; + static IQueryable ApplySkipTakeAndGetPagination( + this IQueryable sortedAndFilteredData, + PagingModel paging, + int filteredRecordsCount, + out PagingResult pagingResult) + { + var itemsCount = filteredRecordsCount; + var currentPage = paging.Page; + var totalPages = (int)Math.Ceiling((double)itemsCount / paging.PerPage); - Uri baseUri; - if (link is null) + if (itemsCount > 0) + { + if (paging.PerPage <= -1) { - var uriBuilder = new UriBuilder(httpContext.Request.Scheme, httpContext.Request.Host.Host, httpContext.Request.Host.Port ?? 80, httpContext.Request.Path.Value) - { - Query = httpContext.Request.QueryString.HasValue ? Uri.EscapeUriString(httpContext.Request.QueryString.Value) : string.Empty - }; - baseUri = uriBuilder.Uri; + currentPage = 1; + totalPages = 1; } - else + else if (paging.Skip > 0) { - baseUri = new Uri(link); + currentPage = (int)Math.Ceiling((double)paging.Skip / paging.PerPage) + 1; + sortedAndFilteredData = sortedAndFilteredData + .Skip(paging.Skip) + .Take(paging.PerPage); } - var links = PagingHelper.GetLinks(baseUri, itemsCount, currentPage, paging.PerPage); - - httpContext.Response.Headers.Add(Constants.Headers.Link, links); - httpContext.Response.Headers.Add(Constants.Headers.TotalRecords, itemsTotalCount.ToString()); - httpContext.Response.Headers.Add(Constants.Headers.TotalFilteredRecords, itemsCount.ToString()); - httpContext.Response.Headers.Add(Constants.Headers.PerPage, paging.PerPage.ToString()); - httpContext.Response.Headers.Add(Constants.Headers.Page, currentPage.ToString()); - httpContext.Response.Headers.Add(Constants.Headers.TotalPages, totalPages.ToString()); - - return sortedAndFilteredData; - } - - static IQueryable ApplySortSearchAndPageFilter( - IQueryable data, - PagingModel paging, - out PagingResult pagingResult, - Expression>? defaultOrder, - Expression>? searchExpression = null) where TSource : class - { - var filteredData = data; - if (!string.IsNullOrWhiteSpace(paging.Search) && searchExpression is not null) - filteredData = data.Where(searchExpression); - - IQueryable sortedAndFilteredData; - var sortCol = typeof(TSource).GetValidPropertyName(paging.SortCol); - if (!string.IsNullOrEmpty(sortCol) && sortCol.Length == paging.SortCol.Trim().Length) + else if (paging.Page > totalPages || paging.Page <= 0) { - sortedAndFilteredData = filteredData.OrderByProperty(sortCol, paging.SortDir); + currentPage = paging.Page; + sortedAndFilteredData = sortedAndFilteredData.Where(x => false); } - else if (defaultOrder is not null) - sortedAndFilteredData = filteredData.OrderBy(defaultOrder); else - sortedAndFilteredData = filteredData; - - var totalItemsCount = data.Count(); - var filteredItemsCount = data == filteredData ? totalItemsCount : sortedAndFilteredData.Count(); - - sortedAndFilteredData = sortedAndFilteredData.ApplySkipTakeAndGetPagination(paging, filteredItemsCount, out pagingResult); - pagingResult.TotalItemsCount = totalItemsCount; - pagingResult.ItemsFilteredCount = filteredItemsCount; - return sortedAndFilteredData; - } - - static IQueryable ApplySkipTakeAndGetPagination( - this IQueryable sorteredAndFilteredData, - PagingModel paging, - int filteredRecordsCount, - out PagingResult pagingResult) - { - var itemsCount = filteredRecordsCount; - var currentPage = paging.Page; - var totalPages = (int)Math.Ceiling((double)itemsCount / paging.PerPage); - - if (itemsCount > 0) { - if (paging.PerPage <= -1) - { - currentPage = 1; - totalPages = 1; - } - else if (paging.Skip > 0) - { - currentPage = (int)Math.Ceiling((double)paging.Skip / paging.PerPage) + 1; - sorteredAndFilteredData = sorteredAndFilteredData - .Skip(paging.Skip) - .Take(paging.PerPage); - } - else if (paging.Page > totalPages || paging.Page <= 0) - { - currentPage = paging.Page; - sorteredAndFilteredData = sorteredAndFilteredData.Where(x => false); - } - else - { - currentPage = paging.Page; - sorteredAndFilteredData = sorteredAndFilteredData - .Skip(paging.PerPage * (currentPage - 1)) - .Take(paging.PerPage); - } + currentPage = paging.Page; + sortedAndFilteredData = sortedAndFilteredData + .Skip(paging.PerPage * (currentPage - 1)) + .Take(paging.PerPage); } - pagingResult = new PagingResult - { - CurrentPage = currentPage, - TotalPages = totalPages - }; - - return sorteredAndFilteredData; } + pagingResult = new PagingResult + { + CurrentPage = currentPage, + TotalPages = totalPages + }; + + return sortedAndFilteredData; } -} \ No newline at end of file +} diff --git a/src/Monq.Core.Paging/Extensions/PagingUriExtensions.cs b/src/Monq.Core.Paging/Extensions/PagingUriExtensions.cs index 79c551e..e6bc90d 100644 --- a/src/Monq.Core.Paging/Extensions/PagingUriExtensions.cs +++ b/src/Monq.Core.Paging/Extensions/PagingUriExtensions.cs @@ -1,116 +1,37 @@ -using Monq.Core.Paging.Models; -using Monq.Core.Paging.Models.DataTable; -using System; -using System.Linq; -using System.Linq.Expressions; +using Monq.Core.Paging.Models; using System.Text; -namespace Monq.Core.Paging.Extensions +namespace Monq.Core.Paging.Extensions; + +public static class PagingUriExtensions { - public static class PagingUriExtensions + /// + /// Получить строковое представление модели . + /// + public static string GetUri(this PagingModel? paging, string url) { - /// - /// Получить строку. - /// - public static string GetUri(this DataTable paging, string url) - { - var pagingModel = paging.GetPagingModel(); - return pagingModel.GetUri(url); - } - - /// - /// Получить строковое представление модели . - /// - public static string GetUri(this PagingModel? paging, string url) - { - var q = url; - - var qb = new StringBuilder(); - if (paging is not null) - { - if (paging.Skip != 0) - qb.Append("&skip=").Append(paging.Skip); - if (paging.Page != 0) - qb.Append("&page=").Append(paging.Page); - if (paging.PerPage != 0) - qb.Append("&perPage=").Append(paging.PerPage); - if (paging.SortCol is not null && !string.IsNullOrWhiteSpace(paging.SortCol)) - qb.Append("&sortCol=").Append(paging.SortCol); - if (paging.SortDir is not null && !string.IsNullOrWhiteSpace(paging.SortDir)) - qb.Append("&sortDir=").Append(paging.SortDir); - if (paging.Search is not null && !string.IsNullOrWhiteSpace(paging.Search)) - qb.Append("&search=").Append(paging.Search.Replace("+", "%2B")); - } - - if (qb.Length == 0) - return q; - - return q.Contains('?') ? q + qb : q + '?' + qb; - } + var q = url; - /// - /// Преобразование DataTable в модель постраничной навигации. - /// - /// Принимаемая модель представления. - /// Модель DataTable. - /// Словарь сопоставлений возможных наименований столбцов из DataTable модели на корректные из . - /// Обнаружено дублирование сопоставления наименования поля из DataTable модели на поле принимаемой view модели. - public static PagingModel GetPagingModel(this DataTable model, - params (string PropName, Expression> PropSelector)[]? sortColMatch) - where T : class + var qb = new StringBuilder(); + if (paging is not null) { - var pagingModel = model.GetPagingModel(); - - if (!string.IsNullOrWhiteSpace(pagingModel.SortCol) && sortColMatch?.Length > 0) - { - try - { - var sortColMatchDict = sortColMatch.ToDictionary(k => k.PropName, v => v.PropSelector); - if (sortColMatchDict.TryGetValue(pagingModel.SortCol, out var propSelector)) - pagingModel.SortCol = propSelector.GetFullPropertyName(); - } - catch (ArgumentException ex) - { - throw new ArgumentException( - "A duplicate mapping of DataTable field to view model field was found.", - ex.ParamName); - } - } - - return pagingModel; + if (paging.Skip != 0) + qb.Append("&skip=").Append(paging.Skip); + if (paging.Page != 0) + qb.Append("&page=").Append(paging.Page); + if (paging.PerPage != 0) + qb.Append("&perPage=").Append(paging.PerPage); + if (paging.SortCol is not null && !string.IsNullOrWhiteSpace(paging.SortCol)) + qb.Append("&sortCol=").Append(paging.SortCol); + if (paging.SortDir is not null && !string.IsNullOrWhiteSpace(paging.SortDir)) + qb.Append("&sortDir=").Append(paging.SortDir); + if (paging.Search is not null && !string.IsNullOrWhiteSpace(paging.Search)) + qb.Append("&search=").Append(paging.Search.Replace("+", "%2B")); } - /// - /// Преобразование DataTable в модель постраничной навигации. - /// - public static PagingModel GetPagingModel(this DataTable model) - { - var sortCol = string.Empty; - var sortDir = string.Empty; - - if (model.Order?.Any() == true && model.Columns?.Any() == true) - { - var orderColumn = model.Order[0].Column; - if (model.Columns.Count > orderColumn) - sortCol = model.Columns[orderColumn].Name; - - sortDir = model.Order[0].Dir; - } + if (qb.Length == 0) + return q; - // REM: Из-за того, что при проверке на номер страницы и поле Skip и поле Page == 0, происходит выборка - // пустого массива данных. Для такой ситуации при Skip == 0 в запросе DataTables проставляется первая страница. - if (model.Start == 0) - model.Page = 1; - - return new PagingModel - { - Page = model.Page, - Skip = model.Start, - PerPage = model.Length, - SortCol = sortCol, - SortDir = sortDir, - Search = model.Search?.Value, - }; - } + return q.Contains('?') ? q + qb : q + '?' + qb; } -} \ No newline at end of file +} diff --git a/src/Monq.Core.Paging/Extensions/QueryableExtensions.cs b/src/Monq.Core.Paging/Extensions/QueryableExtensions.cs index 5502f0d..4f2b507 100644 --- a/src/Monq.Core.Paging/Extensions/QueryableExtensions.cs +++ b/src/Monq.Core.Paging/Extensions/QueryableExtensions.cs @@ -5,123 +5,122 @@ using System.Linq.Expressions; using System.Reflection; -namespace Monq.Core.Paging.Extensions +namespace Monq.Core.Paging.Extensions; + +public static class QueryableExtensions { - public static class QueryableExtensions + static readonly MethodInfo _methodContains = typeof(string).GetMethod(nameof(string.Contains), new[] { typeof(string) })!; + static readonly MethodInfo _methodToString = typeof(object).GetMethod(nameof(ToString))!; + static readonly MethodInfo _methodToLower = typeof(string).GetMethod(nameof(string.ToLower), Type.EmptyTypes)!; + static readonly MethodInfo _methodIsNullOrEmpty = typeof(string).GetMethod(nameof(string.IsNullOrEmpty), new[] { typeof(string) })!; + + static readonly HashSet _supportSearchByStringPropertyTypes = new() + { + typeof(string), + typeof(Guid), + }; + static readonly HashSet _supportSearchByIntNumberPropertyTypes = new() { - static readonly MethodInfo _methodContains = typeof(string).GetMethod(nameof(string.Contains), new[] { typeof(string) })!; - static readonly MethodInfo _methodToString = typeof(object).GetMethod(nameof(ToString))!; - static readonly MethodInfo _methodToLower = typeof(string).GetMethod(nameof(string.ToLower), Type.EmptyTypes)!; - static readonly MethodInfo _methodIsNullOrEmpty = typeof(string).GetMethod(nameof(string.IsNullOrEmpty), new[] { typeof(string) })!; + typeof(int), + typeof(long), + typeof(Guid), + }; + + /// + /// Orderings the by. + /// + /// The type of the source. + /// The data. + /// Name of the field. + /// The dir. + public static IQueryable OrderByProperty(this IQueryable data, string propertyName, string? dir) + { + var par = Expression.Parameter(typeof(TSource), "col"); + var propType = typeof(TSource).GetPropertyType(propertyName); + if (propType is null) + throw new ArgumentException($"{typeof(TSource)} doest not contain a property {propertyName} ", nameof(propertyName)); + + // Проверки на Null требуется проводить только для сортировки по вложенным свойствам. + Expression propExpr = propertyName.Contains(".") + ? par.GetPropertyExpression(propertyName) + : par.GetPropertyExpressionUnSafe(propertyName); + + // Декомпилируем свойства помеченные как Computed, чтобы EF мог их правильно воспринимать. + var lambda = Expression.Lambda(propExpr.Decompile().ExpressionCallsToConstants(), par); + + if (string.IsNullOrEmpty(dir) || dir == "asc") + return data.OrderBy(lambda); + return data.OrderByDescending(lambda); + } - static readonly HashSet _supportSearchByStringPropertyTypes = new() - { - typeof(string), - typeof(Guid), - }; - static readonly HashSet _supportSearchByIntNumberPropertyTypes = new() - { - typeof(int), - typeof(long), - typeof(Guid), - }; - - /// - /// Orderings the by. - /// - /// The type of the source. - /// The data. - /// Name of the field. - /// The dir. - public static IQueryable OrderByProperty(this IQueryable data, string propertyName, string? dir) - { - var par = Expression.Parameter(typeof(TSource), "col"); - var propType = typeof(TSource).GetPropertyType(propertyName); - if (propType is null) - throw new ArgumentException($"{typeof(TSource)} doest not contain a property {propertyName} ", nameof(propertyName)); - - // Проверки на Null требуется проводить только для сортировки по вложенным свойствам. - Expression propExpr = propertyName.Contains(".") - ? par.GetPropertyExpression(propertyName) - : par.GetPropertyExpressionUnSafe(propertyName); - - // Декомпилируем свойства помеченные как Computed, чтобы EF мог их правильно воспринимать. - var lambda = Expression.Lambda(propExpr.Decompile().ExpressionCallsToConstants(), par); - - if (string.IsNullOrEmpty(dir) || dir == "asc") - return data.OrderBy(lambda); - return data.OrderByDescending(lambda); - } + /// + /// Построить выражение для поисковой фразы с параметрами . + /// + /// Тип модели для которой будет построено выражение. + /// Поисковая фраза. + /// Параметры поиска. + internal static Expression>? GetExpressionForSearch(this Searching? searching, string? search) + { + if (string.IsNullOrEmpty(search)) + return null; - /// - /// Построить выражение для поисковой фразы с параметрами . - /// - /// Тип модели для которой будет построено выражение. - /// Поисковая фраза. - /// Параметры поиска. - internal static Expression>? GetExpressionForSearch(this Searching? searching, string? search) - { - if (string.IsNullOrEmpty(search)) - return null; - - searching ??= new Searching(); - - var props = typeof(TSource).GetPublicProperties(searching.Depth, searching.InSearch).ToList(); - var searchByIntProperties = long.TryParse(search, out _) - ? props.Where(p => _supportSearchByIntNumberPropertyTypes.Contains(p.Property.PropertyType)).ToList() - : new List<(string, PropertyInfo)>(); - var searchByStringProperties = props.Where(p => _supportSearchByStringPropertyTypes - .Contains(p.Property.PropertyType)) - .Except(searchByIntProperties) - .ToList(); - - if (!props.Any()) - return null; - - var parameter = Expression.Parameter(typeof(TSource), "t"); - var value = Expression.Constant(search.ToLower(), typeof(string)); - var expList = new List(); - - foreach (var (fullName, prop) in searchByStringProperties) - { - var expAnd = prop.PropertyType == typeof(string) - ? FormExpressionForStringProperty(parameter, value, fullName) - : FormExpressionForValueProperty(parameter, value, fullName); - expList.Add(expAnd); - } - foreach (var (fullName, _) in searchByIntProperties) - { - var expAnd = FormExpressionForValueProperty(parameter, value, fullName); - expList.Add(expAnd); - } - - if (expList.Count == 0) - return null; - - var body = expList.Aggregate(Expression.OrElse); - return Expression.Lambda>(body, parameter); - } + searching ??= new Searching(); - static BinaryExpression FormExpressionForStringProperty(ParameterExpression parameter, ConstantExpression value, string fullName) + var props = typeof(TSource).GetPublicProperties(searching.Depth, searching.InSearch).ToList(); + var searchByIntProperties = long.TryParse(search, out _) + ? props.Where(p => _supportSearchByIntNumberPropertyTypes.Contains(p.Property.PropertyType)).ToList() + : new List<(string, PropertyInfo)>(); + var searchByStringProperties = props.Where(p => _supportSearchByStringPropertyTypes + .Contains(p.Property.PropertyType)) + .Except(searchByIntProperties) + .ToList(); + + if (!props.Any()) + return null; + + var parameter = Expression.Parameter(typeof(TSource), "t"); + var value = Expression.Constant(search.ToLower(), typeof(string)); + var expList = new List(); + + foreach (var (fullName, prop) in searchByStringProperties) { - var expMember = parameter.GetPropertyExpressionUnSafe(fullName); - var expIsNullOrEmpty = Expression.Not(Expression.Call(_methodIsNullOrEmpty, expMember.AddNullConditions())); - var expLower = Expression.Call(expMember, _methodToLower); - var expContains = Expression.Call(expLower, _methodContains, value); - var expAnd = Expression.AndAlso(expIsNullOrEmpty, expContains); - return expAnd; + var expAnd = prop.PropertyType == typeof(string) + ? FormExpressionForStringProperty(parameter, value, fullName) + : FormExpressionForValueProperty(parameter, value, fullName); + expList.Add(expAnd); } - - static BinaryExpression FormExpressionForValueProperty( - ParameterExpression parameter, - ConstantExpression searchValue, - string fullName) + foreach (var (fullName, _) in searchByIntProperties) { - var expMember = parameter.GetPropertyExpression(fullName); - var expMemberToString = Expression.Call(expMember, _methodToString); - var expContains = Expression.Call(expMemberToString, _methodContains, searchValue); - var expAnd = Expression.Equal(expContains, Expression.Constant(true)); - return expAnd; + var expAnd = FormExpressionForValueProperty(parameter, value, fullName); + expList.Add(expAnd); } + + if (expList.Count == 0) + return null; + + var body = expList.Aggregate(Expression.OrElse); + return Expression.Lambda>(body, parameter); + } + + static BinaryExpression FormExpressionForStringProperty(ParameterExpression parameter, ConstantExpression value, string fullName) + { + var expMember = parameter.GetPropertyExpressionUnSafe(fullName); + var expIsNullOrEmpty = Expression.Not(Expression.Call(_methodIsNullOrEmpty, expMember.AddNullConditions())); + var expLower = Expression.Call(expMember, _methodToLower); + var expContains = Expression.Call(expLower, _methodContains, value); + var expAnd = Expression.AndAlso(expIsNullOrEmpty, expContains); + return expAnd; + } + + static BinaryExpression FormExpressionForValueProperty( + ParameterExpression parameter, + ConstantExpression searchValue, + string fullName) + { + var expMember = parameter.GetPropertyExpression(fullName); + var expMemberToString = Expression.Call(expMember, _methodToString); + var expContains = Expression.Call(expMemberToString, _methodContains, searchValue); + var expAnd = Expression.Equal(expContains, Expression.Constant(true)); + return expAnd; } } \ No newline at end of file diff --git a/src/Monq.Core.Paging/Helpers/ExpressionConstantCallVisitor.cs b/src/Monq.Core.Paging/Helpers/ExpressionConstantCallVisitor.cs index 3d253e6..494019f 100644 --- a/src/Monq.Core.Paging/Helpers/ExpressionConstantCallVisitor.cs +++ b/src/Monq.Core.Paging/Helpers/ExpressionConstantCallVisitor.cs @@ -1,26 +1,25 @@ using System; using System.Linq.Expressions; -namespace Monq.Core.Paging.Helpers +namespace Monq.Core.Paging.Helpers; + +/// +/// Преобразует вызываемые методы, которые EF не может преобразововать, в константы +/// +/// +public class ExpressionConstantCallVisitor : ExpressionVisitor { - /// - /// Преобразует вызываемые методы, которые EF не может преобразововать, в константы - /// - /// - public class ExpressionConstantCallVisitor : ExpressionVisitor + public static Expression ExpressionCallsToConstants(Expression expression) { - public static Expression ExpressionCallsToConstants(Expression expression) - { - return new ExpressionConstantCallVisitor().Visit(expression); - } + return new ExpressionConstantCallVisitor().Visit(expression); + } - protected override Expression VisitMethodCall(MethodCallExpression node) + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Object?.Type == typeof(DateTimeOffset)) { - if (node.Object?.Type == typeof(DateTimeOffset)) - { - return Expression.Constant(Expression.Lambda(Expression.Call(node.Object, node.Method)).Compile().DynamicInvoke()); - } - return base.VisitMethodCall(node); + return Expression.Constant(Expression.Lambda(Expression.Call(node.Object, node.Method)).Compile().DynamicInvoke()); } + return base.VisitMethodCall(node); } } \ No newline at end of file diff --git a/src/Monq.Core.Paging/Helpers/PagingHelper.cs b/src/Monq.Core.Paging/Helpers/PagingHelper.cs index 86b3fbe..7316de3 100644 --- a/src/Monq.Core.Paging/Helpers/PagingHelper.cs +++ b/src/Monq.Core.Paging/Helpers/PagingHelper.cs @@ -2,76 +2,75 @@ using System; using System.Text; -namespace Monq.Core.Paging.Helpers +namespace Monq.Core.Paging.Helpers; + +/// +/// Класс формирует ссылки для списков данных +/// +public static class PagingHelper { /// - /// Класс формирует ссылки для списков данных + /// Метод формирует список ссылок постраничной навигации в списке. Реализует https://tools.ietf.org/html/rfc5988 /// - public static class PagingHelper + /// Uri адрес, к которому будет применена постраничная разбивка. + /// Количество записей, для которых ведется постраничная разбивка. + /// Текущая страница. + /// Максимальное количество элементов на одну страницу. + public static string GetLinks(Uri? baseUri, long itemsCount, int currentPage, int perPage) { - /// - /// Метод формирует список ссылок постраничной навигации в списке. Реализует https://tools.ietf.org/html/rfc5988 - /// - /// Uri адрес, к которому будет применена постраничная разбивка. - /// Количество записей, для которых ведется постраничная разбивка. - /// Текущая страница. - /// Максимальное количество элементов на одну страницу. - public static string GetLinks(Uri? baseUri, long itemsCount, int currentPage, int perPage) - { - if (baseUri is null) - throw new ArgumentNullException(nameof(baseUri), $"{nameof(baseUri)} is null."); + if (baseUri is null) + throw new ArgumentNullException(nameof(baseUri), $"{nameof(baseUri)} is null."); - if (currentPage == 0) - currentPage = 1; - if (perPage == 0) - return string.Empty; + if (currentPage == 0) + currentPage = 1; + if (perPage == 0) + return string.Empty; - var linksSb = new StringBuilder(); + var linksSb = new StringBuilder(); - var lastPage = (int)Math.Ceiling((double)itemsCount / perPage); + var lastPage = (int)Math.Ceiling((double)itemsCount / perPage); - var prevPage = (currentPage - 1); - var nextPage = (currentPage + 1); + var prevPage = (currentPage - 1); + var nextPage = (currentPage + 1); - if (prevPage > 0) - { - linksSb.AppendFormat("<{0}>; rel=\"first\",", GetNewUriWithPagingQuery(baseUri, perPage, 1)); - linksSb.AppendFormat("<{0}>; rel=\"prev\"", GetNewUriWithPagingQuery(baseUri, perPage, prevPage)); + if (prevPage > 0) + { + linksSb.AppendFormat("<{0}>; rel=\"first\",", GetNewUriWithPagingQuery(baseUri, perPage, 1)); + linksSb.AppendFormat("<{0}>; rel=\"prev\"", GetNewUriWithPagingQuery(baseUri, perPage, prevPage)); - if (nextPage <= lastPage) - linksSb.Append(","); - } if (nextPage <= lastPage) - { - linksSb.AppendFormat("<{0}>; rel=\"next\",", GetNewUriWithPagingQuery(baseUri, perPage, nextPage)); - linksSb.AppendFormat("<{0}>; rel=\"last\"", GetNewUriWithPagingQuery(baseUri, perPage, lastPage)); - } - return linksSb.ToString(); + linksSb.Append(","); } - - static Uri GetNewUriWithPagingQuery(Uri baseUri, int perPage, int page) + if (nextPage <= lastPage) { - var queryDictionary = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(baseUri.Query); - - if (queryDictionary.ContainsKey("perPage")) - { - queryDictionary["perPage"] = perPage.ToString(); - } - else - queryDictionary.Add("perPage", perPage.ToString()); + linksSb.AppendFormat("<{0}>; rel=\"next\",", GetNewUriWithPagingQuery(baseUri, perPage, nextPage)); + linksSb.AppendFormat("<{0}>; rel=\"last\"", GetNewUriWithPagingQuery(baseUri, perPage, lastPage)); + } + return linksSb.ToString(); + } - if (queryDictionary.ContainsKey("page")) - { - queryDictionary["page"] = page.ToString(); - } - else - queryDictionary.Add("page", page.ToString()); + static Uri GetNewUriWithPagingQuery(Uri baseUri, int perPage, int page) + { + var queryDictionary = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(baseUri.Query); - var newUri = new UriBuilder(baseUri); - var newQueryStr = QueryString.Create(queryDictionary); - newUri.Query = newQueryStr.Value; + if (queryDictionary.ContainsKey("perPage")) + { + queryDictionary["perPage"] = perPage.ToString(); + } + else + queryDictionary.Add("perPage", perPage.ToString()); - return newUri.Uri; + if (queryDictionary.ContainsKey("page")) + { + queryDictionary["page"] = page.ToString(); } + else + queryDictionary.Add("page", page.ToString()); + + var newUri = new UriBuilder(baseUri); + var newQueryStr = QueryString.Create(queryDictionary); + newUri.Query = newQueryStr.Value; + + return newUri.Uri; } } diff --git a/src/Monq.Core.Paging/Models/PagingResponseHeaders.cs b/src/Monq.Core.Paging/Models/PagingResponseHeaders.cs index afa8a60..88450c6 100644 --- a/src/Monq.Core.Paging/Models/PagingResponseHeaders.cs +++ b/src/Monq.Core.Paging/Models/PagingResponseHeaders.cs @@ -1,49 +1,48 @@ -namespace Monq.Core.Paging.Models +namespace Monq.Core.Paging.Models; + +/// +/// Модель данных постраничной навигации в заголовках http-ответа. +/// +public class PagingResponseHeaders { /// - /// Модель данных постраничной навигации в заголовках http-ответа. + /// Общее количество записей. /// - public class PagingResponseHeaders - { - /// - /// Общее количество записей. - /// - public int TotalRecords { get; } + public int TotalRecords { get; } - /// - /// Количество отфильтрованных записей. - /// - public int TotalFilteredRecords { get; } + /// + /// Количество отфильтрованных записей. + /// + public int TotalFilteredRecords { get; } - /// - /// Количество записей на одну страницу. - /// - public int PerPage { get; } + /// + /// Количество записей на одну страницу. + /// + public int PerPage { get; } - /// - /// Номер отображаемой страницы. - /// - public int Page { get; } + /// + /// Номер отображаемой страницы. + /// + public int Page { get; } - /// - /// Общее количество страниц. - /// - public int TotalPages { get; } + /// + /// Общее количество страниц. + /// + public int TotalPages { get; } - internal PagingResponseHeaders() { } + internal PagingResponseHeaders() { } - internal PagingResponseHeaders( - int totalRecords, - int totalFilteredRecords, - int perPage, - int page, - int totalPages) - { - TotalRecords = totalRecords; - TotalFilteredRecords = totalFilteredRecords; - PerPage = perPage; - Page = page; - TotalPages = totalPages; - } + internal PagingResponseHeaders( + int totalRecords, + int totalFilteredRecords, + int perPage, + int page, + int totalPages) + { + TotalRecords = totalRecords; + TotalFilteredRecords = totalFilteredRecords; + PerPage = perPage; + Page = page; + TotalPages = totalPages; } } diff --git a/src/Monq.Core.Paging/Models/Searching.cs b/src/Monq.Core.Paging/Models/Searching.cs index 09ddbbe..7abc786 100644 --- a/src/Monq.Core.Paging/Models/Searching.cs +++ b/src/Monq.Core.Paging/Models/Searching.cs @@ -4,74 +4,73 @@ using System.Linq; using System.Linq.Expressions; -namespace Monq.Core.Paging.Models +namespace Monq.Core.Paging.Models; + +/// +/// Параметры поиска. +/// +public class Searching { - /// - /// Параметры поиска. - /// - public class Searching + public Searching() { - public Searching() - { - } + } - public Searching(SearchType searchType, IEnumerable? propNames = null) - { - SearchType = searchType; - Properties = propNames?.Select(x => x.ToLower()); - } + public Searching(SearchType searchType, IEnumerable? propNames = null) + { + SearchType = searchType; + Properties = propNames?.Select(x => x.ToLower()); + } - public Searching(SearchType searchType, params Expression>[] propNames) - { - SearchType = searchType; - Properties = propNames?.Select(x => x.GetFullPropertyName().ToLower()); - } + public Searching(SearchType searchType, params Expression>[] propNames) + { + SearchType = searchType; + Properties = propNames?.Select(x => x.GetFullPropertyName().ToLower()); + } - public static Searching CreateSearching(SearchType searchType, int depth, params Expression>[] propNames) => - new Searching(searchType, propNames.Select(x => x.GetFullPropertyName())) { Depth = depth }; + public static Searching CreateSearching(SearchType searchType, int depth, params Expression>[] propNames) => + new Searching(searchType, propNames.Select(x => x.GetFullPropertyName())) { Depth = depth }; - public bool InSearch(string propName) => - SearchType switch - { - SearchType.Include => Properties?.Any() == true && Properties.Contains(propName.ToLower()), - SearchType.Exclude => Properties?.Any() != true || !Properties.Contains(propName.ToLower()), - _ => true - }; + public bool InSearch(string propName) => + SearchType switch + { + SearchType.Include => Properties?.Any() == true && Properties.Contains(propName.ToLower()), + SearchType.Exclude => Properties?.Any() != true || !Properties.Contains(propName.ToLower()), + _ => true + }; - /// - /// Глубина поиска. - /// - public int Depth { get; set; } = 1; + /// + /// Глубина поиска. + /// + public int Depth { get; set; } = 1; - /// - /// Свойства, которые необходимо включить/исключить в поиск. - /// - public IEnumerable? Properties { get; protected set; } + /// + /// Свойства, которые необходимо включить/исключить в поиск. + /// + public IEnumerable? Properties { get; protected set; } - /// - /// Gets or sets the type. - /// - public SearchType SearchType { get; protected set; } = SearchType.None; - } + /// + /// Gets or sets the type. + /// + public SearchType SearchType { get; protected set; } = SearchType.None; +} +/// +/// Тип поиска +/// +public enum SearchType +{ /// - /// Тип поиска + /// Поиск по всем полям. /// - public enum SearchType - { - /// - /// Поиск по всем полям. - /// - None, + None, - /// - /// Поиск только по полям указанных в Properties. - /// - Include, + /// + /// Поиск только по полям указанных в Properties. + /// + Include, - /// - /// Поиск по всем полям кроме тех что указаны в Properties. - /// - Exclude - } + /// + /// Поиск по всем полям кроме тех что указаны в Properties. + /// + Exclude } \ No newline at end of file diff --git a/src/Monq.Core.Paging/Monq.Core.Paging.csproj b/src/Monq.Core.Paging/Monq.Core.Paging.csproj index c328609..08d205d 100644 --- a/src/Monq.Core.Paging/Monq.Core.Paging.csproj +++ b/src/Monq.Core.Paging/Monq.Core.Paging.csproj @@ -1,11 +1,11 @@ - + - 3.3.0 + 5.0.0 $(VersionSuffix) $(Version)-$(VersionSuffix) true - netcoreapp3.1;net5.0;net6.0;net7.0 + net7.0;net8.0 Sergey Pismennyi, Dmitry Fokin, Stas Skotnikov, Evgeniy Zyatyna, Antony Vorontsov, melnikovmv MONQ Digital lab Monq.Core.Paging @@ -20,30 +20,23 @@ true snupkg enable - 10.0 - - - - + - - - - - - + + + - - - + + + - - + + From 913f5d30a19dd4885ea6b64bc318c211147ecb4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=9F=D0=B8=D1=81?= =?UTF-8?q?=D1=8C=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9?= Date: Wed, 6 Nov 2024 16:47:29 +0300 Subject: [PATCH 2/3] fix net8 lib version --- src/Monq.Core.Paging/Monq.Core.Paging.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Monq.Core.Paging/Monq.Core.Paging.csproj b/src/Monq.Core.Paging/Monq.Core.Paging.csproj index 08d205d..1f209d8 100644 --- a/src/Monq.Core.Paging/Monq.Core.Paging.csproj +++ b/src/Monq.Core.Paging/Monq.Core.Paging.csproj @@ -32,7 +32,7 @@ - + From 105f16aa04461e88de77367fdc61bbfaea9a1256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=9F=D0=B8=D1=81?= =?UTF-8?q?=D1=8C=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9?= Date: Mon, 11 Nov 2024 15:35:30 +0300 Subject: [PATCH 3/3] up lib --- src/Monq.Core.Paging/Monq.Core.Paging.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Monq.Core.Paging/Monq.Core.Paging.csproj b/src/Monq.Core.Paging/Monq.Core.Paging.csproj index 1f209d8..dadd727 100644 --- a/src/Monq.Core.Paging/Monq.Core.Paging.csproj +++ b/src/Monq.Core.Paging/Monq.Core.Paging.csproj @@ -28,7 +28,7 @@ - +