diff --git a/.circleci/config.yml b/.circleci/config.yml index 09178e49c1..876d46b5c3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,16 +1,17 @@ version: 2.1 orbs: - queue: eddiewebb/queue@2.2.1 + queue: eddiewebb/queue@3.1.4 jobs: build: docker: - - image: ocelot2/circleci-build:8.21.0 + - image: ocelot2/circleci-build:latest + resource_class: medium+ steps: - checkout - run: dotnet tool restore && dotnet cake release: docker: - - image: ocelot2/circleci-build:8.21.0 + - image: ocelot2/circleci-build:latest steps: - checkout - run: dotnet tool restore && dotnet cake --target=Release @@ -18,12 +19,12 @@ workflows: version: 2 main: jobs: - - queue/block_workflow: - time: '20' - only-on-branch: main + # - queue/block_workflow: + # time: '20' + # only-on-branch: main - release: - requires: - - queue/block_workflow + # requires: + # - queue/block_workflow filters: branches: only: main @@ -33,7 +34,7 @@ workflows: filters: branches: only: develop - pr: + PR: jobs: - build: filters: diff --git a/.editorconfig b/.editorconfig index e4e769b528..e8766a5e04 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,15 +1,246 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories root = true -[*] -end_of_line = lf -insert_final_newline = true +# XML files +[*.xml] +indent_style = space +indent_size = 2 +# C# files [*.cs] -end_of_line = lf -indent_style = space -indent_size = 4 -# XML files -[*.xml] +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 indent_style = space -indent_size = 2 +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = true + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_collection_expression = false:suggestion +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true + +# Code-block preferences +csharp_prefer_braces = true:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_primary_constructors = false:suggestion +csharp_style_prefer_top_level_statements = true:silent + +# Expression-level preferences +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# Formatting Rules #### + +# New line preferences +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 + +# Indentation preferences +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 + +# Space preferences +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 + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +[*.{cs,vb}] +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +insert_final_newline = true +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion diff --git a/.gitignore b/.gitignore index 1c30928ce2..f01a1095e0 100644 --- a/.gitignore +++ b/.gitignore @@ -417,5 +417,4 @@ test/Ocelot.AcceptanceTests/ocelot.json # Read the Docs # https://ocelot.readthedocs.io _build/ -_static/ _templates/ diff --git a/Directory.Build.props b/Directory.Build.props deleted file mode 100644 index 4fe1ec98fb..0000000000 --- a/Directory.Build.props +++ /dev/null @@ -1,15 +0,0 @@ - - - latest - git - https://github.com/ThreeMammals/Ocelot - - true - - true - snupkg - - - - - diff --git a/Ocelot.Release.sln b/Ocelot.Release.sln new file mode 100644 index 0000000000..20a76ee837 --- /dev/null +++ b/Ocelot.Release.sln @@ -0,0 +1,231 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5CFB79B7-C9DC-45A4-9A75-625D92471702}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3FA7C349-DBE8-4904-A2CE-015B8869CE6C}" + ProjectSection(SolutionItems) = preProject + .dockerignore = .dockerignore + .editorconfig = .editorconfig + .gitignore = .gitignore + .readthedocs.yaml = .readthedocs.yaml + build.cake = build.cake + build.ps1 = build.ps1 + codeanalysis.ruleset = codeanalysis.ruleset + .circleci\config.yml = .circleci\config.yml + GitVersion.yml = GitVersion.yml + LICENSE.md = LICENSE.md + README.md = README.md + ReleaseNotes.md = ReleaseNotes.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5B401523-36DA-4491-B73A-7590A26E420B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.UnitTests", "test\Ocelot.UnitTests\Ocelot.UnitTests.csproj", "{54E84F1A-E525-4443-96EC-039CBD50C263}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.AcceptanceTests", "test\Ocelot.AcceptanceTests\Ocelot.AcceptanceTests.csproj", "{F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.ManualTest", "test\Ocelot.ManualTest\Ocelot.ManualTest.csproj", "{02BBF4C5-517E-4157-8D21-4B8B9E118B7A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Benchmarks", "test\Ocelot.Benchmarks\Ocelot.Benchmarks.csproj", "{106B49E6-95F6-4A7B-B81C-96BFA74AF035}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.IntegrationTests", "test\Ocelot.IntegrationTests\Ocelot.IntegrationTests.csproj", "{D4575572-99CA-4530-8737-C296EDA326F8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Testing", "test\Ocelot.Testing\Ocelot.Testing.csproj", "{AE6BCCBD-0687-4C58-B30F-4ABBC6422087}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot", "src\Ocelot\Ocelot.csproj", "{D6DF4206-0DBA-41D8-884D-C3E08290FDBB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Administration", "src\Ocelot.Administration\Ocelot.Administration.csproj", "{F69CEF43-27D2-4940-A47A-FCA879E371BC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Cache.CacheManager", "src\Ocelot.Cache.CacheManager\Ocelot.Cache.CacheManager.csproj", "{EB9F438F-062E-499F-B6EA-4412BEF6D74C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Consul", "src\Ocelot.Provider.Consul\Ocelot.Provider.Consul.csproj", "{02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Eureka", "src\Ocelot.Provider.Eureka\Ocelot.Provider.Eureka.csproj", "{9BBD3586-145C-4FA0-91C5-9ED58287D753}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Kubernetes", "src\Ocelot.Provider.Kubernetes\Ocelot.Provider.Kubernetes.csproj", "{72C8E528-B4F5-45CE-8A06-CD3787364856}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Polly", "src\Ocelot.Provider.Polly\Ocelot.Provider.Polly.csproj", "{1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Tracing.Butterfly", "src\Ocelot.Tracing.Butterfly\Ocelot.Tracing.Butterfly.csproj", "{6045E23D-669C-4F27-AF8E-8EEE6DB3557F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Tracing.OpenTracing", "src\Ocelot.Tracing.OpenTracing\Ocelot.Tracing.OpenTracing.csproj", "{11C622AD-8C0A-4CF4-811B-3DBB76550797}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "samples", "{8FA0CBA0-0338-48EB-B37F-83CA5022237C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.AdministrationApi", "samples\Administration\Ocelot.Samples.AdministrationApi.csproj", "{A7F0CAFA-AECB-43CA-BE89-5F5B728E7C22}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Basic.ApiGateway", "samples\Basic\Ocelot.Samples.Basic.ApiGateway.csproj", "{F00C73F4-019D-490D-8194-CA1754D717FA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Eureka.ApiGateway", "samples\Eureka\ApiGateway\Ocelot.Samples.Eureka.ApiGateway.csproj", "{FECB0C8B-5778-4441-B10E-0C815F5106D5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Eureka.DownstreamService", "samples\Eureka\DownstreamService\Ocelot.Samples.Eureka.DownstreamService.csproj", "{28AD7065-8DB1-4711-83BF-9EA47D75F8F7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.GraphQL", "samples\GraphQL\Ocelot.Samples.GraphQL.csproj", "{869EE931-7E4A-40AA-ADDD-D20DF34C3BB3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Kubernetes.ApiGateway", "samples\Kubernetes\ApiGateway\Ocelot.Samples.Kubernetes.ApiGateway.csproj", "{681B6E08-114D-4B9B-8F82-E370CA29B8EC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Kubernetes.DownstreamService", "samples\Kubernetes\DownstreamService\Ocelot.Samples.Kubernetes.DownstreamService.csproj", "{161DD558-993D-491B-AD20-966127D71E49}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.OpenTracing", "samples\OpenTracing\Ocelot.Samples.OpenTracing.csproj", "{DF9EFF21-58D3-428D-8A33-ACFA24E9B6E8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.ApiGateway", "samples\ServiceDiscovery\ApiGateway\Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj", "{F25EA682-A763-431B-9D88-012A388D3618}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.DownstreamService", "samples\ServiceDiscovery\DownstreamService\Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj", "{DCBD0AB5-85DD-4F28-9166-0A23969E19EC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceFabric.ApiGateway", "samples\ServiceFabric\ApiGateway\Ocelot.Samples.ServiceFabric.ApiGateway.csproj", "{D991C694-01F0-4F04-8135-5C133DC8E029}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceFabric.DownstreamService", "samples\ServiceFabric\DownstreamService\Ocelot.Samples.ServiceFabric.DownstreamService.csproj", "{AD09D124-7DD7-4C9E-9BCC-782B579B1786}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Release|Any CPU.Build.0 = Release|Any CPU + {54E84F1A-E525-4443-96EC-039CBD50C263}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {54E84F1A-E525-4443-96EC-039CBD50C263}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54E84F1A-E525-4443-96EC-039CBD50C263}.Release|Any CPU.ActiveCfg = Release|Any CPU + {54E84F1A-E525-4443-96EC-039CBD50C263}.Release|Any CPU.Build.0 = Release|Any CPU + {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}.Release|Any CPU.Build.0 = Release|Any CPU + {02BBF4C5-517E-4157-8D21-4B8B9E118B7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {02BBF4C5-517E-4157-8D21-4B8B9E118B7A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {02BBF4C5-517E-4157-8D21-4B8B9E118B7A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {02BBF4C5-517E-4157-8D21-4B8B9E118B7A}.Release|Any CPU.Build.0 = Release|Any CPU + {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Debug|Any CPU.Build.0 = Debug|Any CPU + {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Release|Any CPU.ActiveCfg = Release|Any CPU + {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Release|Any CPU.Build.0 = Release|Any CPU + {D4575572-99CA-4530-8737-C296EDA326F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4575572-99CA-4530-8737-C296EDA326F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4575572-99CA-4530-8737-C296EDA326F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4575572-99CA-4530-8737-C296EDA326F8}.Release|Any CPU.Build.0 = Release|Any CPU + {F69CEF43-27D2-4940-A47A-FCA879E371BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F69CEF43-27D2-4940-A47A-FCA879E371BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F69CEF43-27D2-4940-A47A-FCA879E371BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F69CEF43-27D2-4940-A47A-FCA879E371BC}.Release|Any CPU.Build.0 = Release|Any CPU + {EB9F438F-062E-499F-B6EA-4412BEF6D74C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB9F438F-062E-499F-B6EA-4412BEF6D74C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB9F438F-062E-499F-B6EA-4412BEF6D74C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB9F438F-062E-499F-B6EA-4412BEF6D74C}.Release|Any CPU.Build.0 = Release|Any CPU + {02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0}.Release|Any CPU.Build.0 = Release|Any CPU + {9BBD3586-145C-4FA0-91C5-9ED58287D753}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9BBD3586-145C-4FA0-91C5-9ED58287D753}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9BBD3586-145C-4FA0-91C5-9ED58287D753}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9BBD3586-145C-4FA0-91C5-9ED58287D753}.Release|Any CPU.Build.0 = Release|Any CPU + {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Release|Any CPU.Build.0 = Release|Any CPU + {6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Release|Any CPU.Build.0 = Release|Any CPU + {72C8E528-B4F5-45CE-8A06-CD3787364856}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72C8E528-B4F5-45CE-8A06-CD3787364856}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72C8E528-B4F5-45CE-8A06-CD3787364856}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72C8E528-B4F5-45CE-8A06-CD3787364856}.Release|Any CPU.Build.0 = Release|Any CPU + {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Release|Any CPU.Build.0 = Release|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Release|Any CPU.Build.0 = Release|Any CPU + {A7F0CAFA-AECB-43CA-BE89-5F5B728E7C22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7F0CAFA-AECB-43CA-BE89-5F5B728E7C22}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7F0CAFA-AECB-43CA-BE89-5F5B728E7C22}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7F0CAFA-AECB-43CA-BE89-5F5B728E7C22}.Release|Any CPU.Build.0 = Release|Any CPU + {F00C73F4-019D-490D-8194-CA1754D717FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F00C73F4-019D-490D-8194-CA1754D717FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F00C73F4-019D-490D-8194-CA1754D717FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F00C73F4-019D-490D-8194-CA1754D717FA}.Release|Any CPU.Build.0 = Release|Any CPU + {FECB0C8B-5778-4441-B10E-0C815F5106D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FECB0C8B-5778-4441-B10E-0C815F5106D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FECB0C8B-5778-4441-B10E-0C815F5106D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FECB0C8B-5778-4441-B10E-0C815F5106D5}.Release|Any CPU.Build.0 = Release|Any CPU + {28AD7065-8DB1-4711-83BF-9EA47D75F8F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28AD7065-8DB1-4711-83BF-9EA47D75F8F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28AD7065-8DB1-4711-83BF-9EA47D75F8F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28AD7065-8DB1-4711-83BF-9EA47D75F8F7}.Release|Any CPU.Build.0 = Release|Any CPU + {869EE931-7E4A-40AA-ADDD-D20DF34C3BB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {869EE931-7E4A-40AA-ADDD-D20DF34C3BB3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {869EE931-7E4A-40AA-ADDD-D20DF34C3BB3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {869EE931-7E4A-40AA-ADDD-D20DF34C3BB3}.Release|Any CPU.Build.0 = Release|Any CPU + {681B6E08-114D-4B9B-8F82-E370CA29B8EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {681B6E08-114D-4B9B-8F82-E370CA29B8EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {681B6E08-114D-4B9B-8F82-E370CA29B8EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {681B6E08-114D-4B9B-8F82-E370CA29B8EC}.Release|Any CPU.Build.0 = Release|Any CPU + {161DD558-993D-491B-AD20-966127D71E49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {161DD558-993D-491B-AD20-966127D71E49}.Debug|Any CPU.Build.0 = Debug|Any CPU + {161DD558-993D-491B-AD20-966127D71E49}.Release|Any CPU.ActiveCfg = Release|Any CPU + {161DD558-993D-491B-AD20-966127D71E49}.Release|Any CPU.Build.0 = Release|Any CPU + {DF9EFF21-58D3-428D-8A33-ACFA24E9B6E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF9EFF21-58D3-428D-8A33-ACFA24E9B6E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF9EFF21-58D3-428D-8A33-ACFA24E9B6E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF9EFF21-58D3-428D-8A33-ACFA24E9B6E8}.Release|Any CPU.Build.0 = Release|Any CPU + {F25EA682-A763-431B-9D88-012A388D3618}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F25EA682-A763-431B-9D88-012A388D3618}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F25EA682-A763-431B-9D88-012A388D3618}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F25EA682-A763-431B-9D88-012A388D3618}.Release|Any CPU.Build.0 = Release|Any CPU + {DCBD0AB5-85DD-4F28-9166-0A23969E19EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DCBD0AB5-85DD-4F28-9166-0A23969E19EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DCBD0AB5-85DD-4F28-9166-0A23969E19EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DCBD0AB5-85DD-4F28-9166-0A23969E19EC}.Release|Any CPU.Build.0 = Release|Any CPU + {D991C694-01F0-4F04-8135-5C133DC8E029}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D991C694-01F0-4F04-8135-5C133DC8E029}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D991C694-01F0-4F04-8135-5C133DC8E029}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D991C694-01F0-4F04-8135-5C133DC8E029}.Release|Any CPU.Build.0 = Release|Any CPU + {AD09D124-7DD7-4C9E-9BCC-782B579B1786}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD09D124-7DD7-4C9E-9BCC-782B579B1786}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD09D124-7DD7-4C9E-9BCC-782B579B1786}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD09D124-7DD7-4C9E-9BCC-782B579B1786}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {54E84F1A-E525-4443-96EC-039CBD50C263} = {5B401523-36DA-4491-B73A-7590A26E420B} + {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52} = {5B401523-36DA-4491-B73A-7590A26E420B} + {02BBF4C5-517E-4157-8D21-4B8B9E118B7A} = {5B401523-36DA-4491-B73A-7590A26E420B} + {106B49E6-95F6-4A7B-B81C-96BFA74AF035} = {5B401523-36DA-4491-B73A-7590A26E420B} + {D4575572-99CA-4530-8737-C296EDA326F8} = {5B401523-36DA-4491-B73A-7590A26E420B} + {F69CEF43-27D2-4940-A47A-FCA879E371BC} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {EB9F438F-062E-499F-B6EA-4412BEF6D74C} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {9BBD3586-145C-4FA0-91C5-9ED58287D753} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {6045E23D-669C-4F27-AF8E-8EEE6DB3557F} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {72C8E528-B4F5-45CE-8A06-CD3787364856} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {11C622AD-8C0A-4CF4-811B-3DBB76550797} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087} = {5B401523-36DA-4491-B73A-7590A26E420B} + {A7F0CAFA-AECB-43CA-BE89-5F5B728E7C22} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {F00C73F4-019D-490D-8194-CA1754D717FA} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {FECB0C8B-5778-4441-B10E-0C815F5106D5} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {28AD7065-8DB1-4711-83BF-9EA47D75F8F7} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {869EE931-7E4A-40AA-ADDD-D20DF34C3BB3} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {681B6E08-114D-4B9B-8F82-E370CA29B8EC} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {161DD558-993D-491B-AD20-966127D71E49} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {DF9EFF21-58D3-428D-8A33-ACFA24E9B6E8} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {F25EA682-A763-431B-9D88-012A388D3618} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {DCBD0AB5-85DD-4F28-9166-0A23969E19EC} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {D991C694-01F0-4F04-8135-5C133DC8E029} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {AD09D124-7DD7-4C9E-9BCC-782B579B1786} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {21476EFF-778A-4F97-8A56-D1AF1CEC0C48} + EndGlobalSection +EndGlobal diff --git a/Ocelot.sln b/Ocelot.sln index e40f83cfb3..f09456c44e 100644 --- a/Ocelot.sln +++ b/Ocelot.sln @@ -1,7 +1,6 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.8.34309.116 +VisualStudioVersion = 17.9.34728.123 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5CFB79B7-C9DC-45A4-9A75-625D92471702}" EndProject @@ -23,8 +22,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5B401523-36DA-4491-B73A-7590A26E420B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot", "src\Ocelot\Ocelot.csproj", "{D6DF4206-0DBA-41D8-884D-C3E08290FDBB}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.UnitTests", "test\Ocelot.UnitTests\Ocelot.UnitTests.csproj", "{54E84F1A-E525-4443-96EC-039CBD50C263}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.AcceptanceTests", "test\Ocelot.AcceptanceTests\Ocelot.AcceptanceTests.csproj", "{F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}" @@ -35,6 +32,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Benchmarks", "test\O EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.IntegrationTests", "test\Ocelot.IntegrationTests\Ocelot.IntegrationTests.csproj", "{D4575572-99CA-4530-8737-C296EDA326F8}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Testing", "test\Ocelot.Testing\Ocelot.Testing.csproj", "{AE6BCCBD-0687-4C58-B30F-4ABBC6422087}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot", "src\Ocelot\Ocelot.csproj", "{D6DF4206-0DBA-41D8-884D-C3E08290FDBB}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Administration", "src\Ocelot.Administration\Ocelot.Administration.csproj", "{F69CEF43-27D2-4940-A47A-FCA879E371BC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Cache.CacheManager", "src\Ocelot.Cache.CacheManager\Ocelot.Cache.CacheManager.csproj", "{EB9F438F-062E-499F-B6EA-4412BEF6D74C}" @@ -43,68 +44,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Consul", "s EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Eureka", "src\Ocelot.Provider.Eureka\Ocelot.Provider.Eureka.csproj", "{9BBD3586-145C-4FA0-91C5-9ED58287D753}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Polly", "src\Ocelot.Provider.Polly\Ocelot.Provider.Polly.csproj", "{1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Tracing.Butterfly", "src\Ocelot.Tracing.Butterfly\Ocelot.Tracing.Butterfly.csproj", "{6045E23D-669C-4F27-AF8E-8EEE6DB3557F}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Kubernetes", "src\Ocelot.Provider.Kubernetes\Ocelot.Provider.Kubernetes.csproj", "{72C8E528-B4F5-45CE-8A06-CD3787364856}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{8FA0CBA0-0338-48EB-B37F-83CA5022237C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.OcelotBasic.ApiGateway", "samples\OcelotBasic\Ocelot.Samples.OcelotBasic.ApiGateway.csproj", "{ED0B3A09-112B-4BA4-82D6-11569BC7A99B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdministrationApi", "samples\AdministrationApi\AdministrationApi.csproj", "{B180F8AE-2F8F-44F9-9E5D-FE65B84B742E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OcelotGraphQL", "samples\OcelotGraphQL\OcelotGraphQL.csproj", "{F43429C3-EC49-464F-9423-9118A36E8FE3}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eureka", "eureka", "{F1CF6F06-5A34-4A6A-8C19-003A78AB0DCF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiGateway", "samples\OcelotEureka\ApiGateway\ApiGateway.csproj", "{48B3DD3C-7F4D-40C1-A104-3BF9EF4ACE29}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DownstreamService", "samples\OcelotEureka\DownstreamService\DownstreamService.csproj", "{32ADF9B3-CBFA-4607-8A8E-1532D90A7197}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "k8s", "k8s", "{4B706988-4817-43A8-ABE1-32A67998C2C8}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.OcelotKube.ApiGateway", "samples\OcelotKube\ApiGateway\Ocelot.Samples.OcelotKube.ApiGateway.csproj", "{8500055B-2C51-4CF1-A6EE-F05BB3E9BF16}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.OcelotKube.DownstreamService", "samples\OcelotKube\DownstreamService\Ocelot.Samples.OcelotKube.DownstreamService.csproj", "{7B319B8C-8155-4779-BD93-5ABD05CA2AB6}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "service-fabric", "service-fabric", "{B412628F-C325-47E1-A8D9-873DE04C8AF5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OcelotApplicationApiGateway", "samples\OcelotServiceFabric\src\OcelotApplicationApiGateway\OcelotApplicationApiGateway.csproj", "{8E6DAE6E-E9B1-433A-80C3-1E2640FBA590}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OcelotApplicationService", "samples\OcelotServiceFabric\src\OcelotApplicationService\OcelotApplicationService.csproj", "{33BE6D88-F188-4E60-83AC-3C4B94D24675}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "administration", "administration", "{1F1F324D-6EA4-4E63-A6A7-C6053F412F1A}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "basic", "basic", "{ED066001-BAF7-4117-9884-DF591A56347D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Polly", "src\Ocelot.Provider.Polly\Ocelot.Provider.Polly.csproj", "{1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "graphql", "graphql", "{C15CD120-5F8D-41DE-9B21-00E3EA77D6C1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Tracing.Butterfly", "src\Ocelot.Tracing.Butterfly\Ocelot.Tracing.Butterfly.csproj", "{6045E23D-669C-4F27-AF8E-8EEE6DB3557F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Tracing.OpenTracing", "src\Ocelot.Tracing.OpenTracing\Ocelot.Tracing.OpenTracing.csproj", "{11C622AD-8C0A-4CF4-811B-3DBB76550797}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "open-tracing", "open-tracing", "{731C6A8A-69ED-445C-A132-C638AA93F9C7}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OcelotOpenTracing", "samples\OcelotOpenTracing\OcelotOpenTracing.csproj", "{C9427E78-4281-4F59-A66E-17C0B66550E5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "service-discovery", "service-discovery", "{25C30AAA-12DD-4BA5-A53F-9271E54EBAB7}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.ApiGateway", "samples\OcelotServiceDiscovery\ApiGateway\Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj", "{D37209EA-C13E-42AE-B851-A8604F1FCD0E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.DownstreamService", "samples\OcelotServiceDiscovery\DownstreamService\Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj", "{E2AC741A-4120-4D59-B5E4-16382ED45E8D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Testing", "test\Ocelot.Testing\Ocelot.Testing.csproj", "{AE6BCCBD-0687-4C58-B30F-4ABBC6422087}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Release|Any CPU.Build.0 = Release|Any CPU {54E84F1A-E525-4443-96EC-039CBD50C263}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {54E84F1A-E525-4443-96EC-039CBD50C263}.Debug|Any CPU.Build.0 = Debug|Any CPU {54E84F1A-E525-4443-96EC-039CBD50C263}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -125,6 +78,14 @@ Global {D4575572-99CA-4530-8737-C296EDA326F8}.Debug|Any CPU.Build.0 = Debug|Any CPU {D4575572-99CA-4530-8737-C296EDA326F8}.Release|Any CPU.ActiveCfg = Release|Any CPU {D4575572-99CA-4530-8737-C296EDA326F8}.Release|Any CPU.Build.0 = Release|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Release|Any CPU.Build.0 = Release|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Release|Any CPU.Build.0 = Release|Any CPU {F69CEF43-27D2-4940-A47A-FCA879E371BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F69CEF43-27D2-4940-A47A-FCA879E371BC}.Debug|Any CPU.Build.0 = Debug|Any CPU {F69CEF43-27D2-4940-A47A-FCA879E371BC}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -141,6 +102,10 @@ Global {9BBD3586-145C-4FA0-91C5-9ED58287D753}.Debug|Any CPU.Build.0 = Debug|Any CPU {9BBD3586-145C-4FA0-91C5-9ED58287D753}.Release|Any CPU.ActiveCfg = Release|Any CPU {9BBD3586-145C-4FA0-91C5-9ED58287D753}.Release|Any CPU.Build.0 = Release|Any CPU + {72C8E528-B4F5-45CE-8A06-CD3787364856}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72C8E528-B4F5-45CE-8A06-CD3787364856}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72C8E528-B4F5-45CE-8A06-CD3787364856}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72C8E528-B4F5-45CE-8A06-CD3787364856}.Release|Any CPU.Build.0 = Release|Any CPU {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Debug|Any CPU.Build.0 = Debug|Any CPU {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -149,106 +114,30 @@ Global {6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Debug|Any CPU.Build.0 = Debug|Any CPU {6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Release|Any CPU.ActiveCfg = Release|Any CPU {6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Release|Any CPU.Build.0 = Release|Any CPU - {72C8E528-B4F5-45CE-8A06-CD3787364856}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {72C8E528-B4F5-45CE-8A06-CD3787364856}.Debug|Any CPU.Build.0 = Debug|Any CPU - {72C8E528-B4F5-45CE-8A06-CD3787364856}.Release|Any CPU.ActiveCfg = Release|Any CPU - {72C8E528-B4F5-45CE-8A06-CD3787364856}.Release|Any CPU.Build.0 = Release|Any CPU - {ED0B3A09-112B-4BA4-82D6-11569BC7A99B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ED0B3A09-112B-4BA4-82D6-11569BC7A99B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ED0B3A09-112B-4BA4-82D6-11569BC7A99B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ED0B3A09-112B-4BA4-82D6-11569BC7A99B}.Release|Any CPU.Build.0 = Release|Any CPU - {B180F8AE-2F8F-44F9-9E5D-FE65B84B742E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B180F8AE-2F8F-44F9-9E5D-FE65B84B742E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B180F8AE-2F8F-44F9-9E5D-FE65B84B742E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B180F8AE-2F8F-44F9-9E5D-FE65B84B742E}.Release|Any CPU.Build.0 = Release|Any CPU - {F43429C3-EC49-464F-9423-9118A36E8FE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F43429C3-EC49-464F-9423-9118A36E8FE3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F43429C3-EC49-464F-9423-9118A36E8FE3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F43429C3-EC49-464F-9423-9118A36E8FE3}.Release|Any CPU.Build.0 = Release|Any CPU - {48B3DD3C-7F4D-40C1-A104-3BF9EF4ACE29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {48B3DD3C-7F4D-40C1-A104-3BF9EF4ACE29}.Debug|Any CPU.Build.0 = Debug|Any CPU - {48B3DD3C-7F4D-40C1-A104-3BF9EF4ACE29}.Release|Any CPU.ActiveCfg = Release|Any CPU - {48B3DD3C-7F4D-40C1-A104-3BF9EF4ACE29}.Release|Any CPU.Build.0 = Release|Any CPU - {32ADF9B3-CBFA-4607-8A8E-1532D90A7197}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {32ADF9B3-CBFA-4607-8A8E-1532D90A7197}.Debug|Any CPU.Build.0 = Debug|Any CPU - {32ADF9B3-CBFA-4607-8A8E-1532D90A7197}.Release|Any CPU.ActiveCfg = Release|Any CPU - {32ADF9B3-CBFA-4607-8A8E-1532D90A7197}.Release|Any CPU.Build.0 = Release|Any CPU - {8500055B-2C51-4CF1-A6EE-F05BB3E9BF16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8500055B-2C51-4CF1-A6EE-F05BB3E9BF16}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8500055B-2C51-4CF1-A6EE-F05BB3E9BF16}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8500055B-2C51-4CF1-A6EE-F05BB3E9BF16}.Release|Any CPU.Build.0 = Release|Any CPU - {7B319B8C-8155-4779-BD93-5ABD05CA2AB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7B319B8C-8155-4779-BD93-5ABD05CA2AB6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7B319B8C-8155-4779-BD93-5ABD05CA2AB6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7B319B8C-8155-4779-BD93-5ABD05CA2AB6}.Release|Any CPU.Build.0 = Release|Any CPU - {8E6DAE6E-E9B1-433A-80C3-1E2640FBA590}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8E6DAE6E-E9B1-433A-80C3-1E2640FBA590}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8E6DAE6E-E9B1-433A-80C3-1E2640FBA590}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8E6DAE6E-E9B1-433A-80C3-1E2640FBA590}.Release|Any CPU.Build.0 = Release|Any CPU - {33BE6D88-F188-4E60-83AC-3C4B94D24675}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {33BE6D88-F188-4E60-83AC-3C4B94D24675}.Debug|Any CPU.Build.0 = Debug|Any CPU - {33BE6D88-F188-4E60-83AC-3C4B94D24675}.Release|Any CPU.ActiveCfg = Release|Any CPU - {33BE6D88-F188-4E60-83AC-3C4B94D24675}.Release|Any CPU.Build.0 = Release|Any CPU {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Debug|Any CPU.Build.0 = Debug|Any CPU {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Release|Any CPU.ActiveCfg = Release|Any CPU {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Release|Any CPU.Build.0 = Release|Any CPU - {C9427E78-4281-4F59-A66E-17C0B66550E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C9427E78-4281-4F59-A66E-17C0B66550E5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C9427E78-4281-4F59-A66E-17C0B66550E5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C9427E78-4281-4F59-A66E-17C0B66550E5}.Release|Any CPU.Build.0 = Release|Any CPU - {D37209EA-C13E-42AE-B851-A8604F1FCD0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D37209EA-C13E-42AE-B851-A8604F1FCD0E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D37209EA-C13E-42AE-B851-A8604F1FCD0E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D37209EA-C13E-42AE-B851-A8604F1FCD0E}.Release|Any CPU.Build.0 = Release|Any CPU - {E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Release|Any CPU.Build.0 = Release|Any CPU - {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {D6DF4206-0DBA-41D8-884D-C3E08290FDBB} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {54E84F1A-E525-4443-96EC-039CBD50C263} = {5B401523-36DA-4491-B73A-7590A26E420B} {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52} = {5B401523-36DA-4491-B73A-7590A26E420B} {02BBF4C5-517E-4157-8D21-4B8B9E118B7A} = {5B401523-36DA-4491-B73A-7590A26E420B} {106B49E6-95F6-4A7B-B81C-96BFA74AF035} = {5B401523-36DA-4491-B73A-7590A26E420B} {D4575572-99CA-4530-8737-C296EDA326F8} = {5B401523-36DA-4491-B73A-7590A26E420B} + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087} = {5B401523-36DA-4491-B73A-7590A26E420B} + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {F69CEF43-27D2-4940-A47A-FCA879E371BC} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {EB9F438F-062E-499F-B6EA-4412BEF6D74C} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {9BBD3586-145C-4FA0-91C5-9ED58287D753} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {72C8E528-B4F5-45CE-8A06-CD3787364856} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {6045E23D-669C-4F27-AF8E-8EEE6DB3557F} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} - {72C8E528-B4F5-45CE-8A06-CD3787364856} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} - {ED0B3A09-112B-4BA4-82D6-11569BC7A99B} = {ED066001-BAF7-4117-9884-DF591A56347D} - {B180F8AE-2F8F-44F9-9E5D-FE65B84B742E} = {1F1F324D-6EA4-4E63-A6A7-C6053F412F1A} - {F43429C3-EC49-464F-9423-9118A36E8FE3} = {C15CD120-5F8D-41DE-9B21-00E3EA77D6C1} - {F1CF6F06-5A34-4A6A-8C19-003A78AB0DCF} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} - {48B3DD3C-7F4D-40C1-A104-3BF9EF4ACE29} = {F1CF6F06-5A34-4A6A-8C19-003A78AB0DCF} - {32ADF9B3-CBFA-4607-8A8E-1532D90A7197} = {F1CF6F06-5A34-4A6A-8C19-003A78AB0DCF} - {4B706988-4817-43A8-ABE1-32A67998C2C8} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} - {8500055B-2C51-4CF1-A6EE-F05BB3E9BF16} = {4B706988-4817-43A8-ABE1-32A67998C2C8} - {7B319B8C-8155-4779-BD93-5ABD05CA2AB6} = {4B706988-4817-43A8-ABE1-32A67998C2C8} - {B412628F-C325-47E1-A8D9-873DE04C8AF5} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} - {8E6DAE6E-E9B1-433A-80C3-1E2640FBA590} = {B412628F-C325-47E1-A8D9-873DE04C8AF5} - {33BE6D88-F188-4E60-83AC-3C4B94D24675} = {B412628F-C325-47E1-A8D9-873DE04C8AF5} - {1F1F324D-6EA4-4E63-A6A7-C6053F412F1A} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} - {ED066001-BAF7-4117-9884-DF591A56347D} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} - {C15CD120-5F8D-41DE-9B21-00E3EA77D6C1} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} {11C622AD-8C0A-4CF4-811B-3DBB76550797} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} - {731C6A8A-69ED-445C-A132-C638AA93F9C7} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} - {C9427E78-4281-4F59-A66E-17C0B66550E5} = {731C6A8A-69ED-445C-A132-C638AA93F9C7} - {25C30AAA-12DD-4BA5-A53F-9271E54EBAB7} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} - {D37209EA-C13E-42AE-B851-A8604F1FCD0E} = {25C30AAA-12DD-4BA5-A53F-9271E54EBAB7} - {E2AC741A-4120-4D59-B5E4-16382ED45E8D} = {25C30AAA-12DD-4BA5-A53F-9271E54EBAB7} - {AE6BCCBD-0687-4C58-B30F-4ABBC6422087} = {5B401523-36DA-4491-B73A-7590A26E420B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {21476EFF-778A-4F97-8A56-D1AF1CEC0C48} diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 6698eb051f..48db3c3bfb 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,63 +1,31 @@ -## November-December 2023 (version {0}) aka [Sunny Koliada](https://www.google.com/search?q=winter+solstice) release -> Codenamed as **[Sunny Koliada](https://www.bing.com/search?q=winter+solstice)** +## Hotfix release (version {0}) for #2031 issue +> Route path template placeholders and their validation rules -### Focus On +Special thanks to **[Guillaume Gnaegi](https://github.com/ggnaegi)** and [Fabrizio Mancin](https://github.com/Fabman08)! -
- System performance. System core performance review, redesign of system core related to routing and content streaming +### About +The bug is related to the [Placeholders](https://ocelot.readthedocs.io/en/latest/features/routing.html#placeholders) feature in [Configuration](https://ocelot.readthedocs.io/en/latest/features/configuration.html) and [Routing](https://ocelot.readthedocs.io/en/latest/features/routing.html). +The bug was introduced in version [23.2.0](https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0) as a part of PR #1927. - - Modification of the `RequestMapper` with a brand new `StreamHttpContent` class, in `Ocelot.Request.Mapper` namespace. The request body is no longer copied when it is handled by the API gateway, avoiding Out-of-Memory issues in pods/containers. This significantly reduces the gateway's memory consumption, and allows you to transfer content larger than 2 GB in streaming scenarios. - - Introduction of a new Message Invoker pool, in `Ocelot.Requester` namespace. We have replaced the [HttpClient](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient) class with [HttpMessageInvoker](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpmessageinvoker), which is the base class for `HttpClient`. The overall logic for managing the pool has been simplified, resulting in a reduction in the number of CPU cycles. - - Full HTTP content buffering is deactivated, resulting in a 50% reduction in memory consumption and a performance improvement of around 10%. Content is no longer copied on the API gateway, avoiding Out-of-Memory issues. - - **TODO** Include screenshots from Production... -
+### Breaking Change +The new [validation rules](https://github.com/ThreeMammals/Ocelot/blob/23.2.0/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs#L45-L50) of the `FileConfigurationFluentValidator` class do not allow the Ocelot app to start when implicit [placeholders](https://ocelot.readthedocs.io/en/latest/features/routing.html#placeholders) are defined in custom implementations, such as middlewares, delegating handlers, and replaced services in the dependency injection (DI) container. +These new rules are capable of validating explicit [placeholders](https://ocelot.readthedocs.io/en/latest/features/routing.html#placeholders) only within the `UpstreamPathTemplate` and `DownstreamPathTemplate` properties. Unfortunately, they cannot oversee implicit placeholders in custom implementations, and they do not validate early during the Ocelot app startup process. -
- Ocelot extra packages. Total 3 Ocelot packs were updated - - - [Ocelot.Cache.CacheManager](https://github.com/ThreeMammals/Ocelot/tree/main/src/Ocelot.Cache.CacheManager): Introduced default cache key generator with improved performance (the `DefaultCacheKeyGenerator` class). Old version of `CacheKeyGenerator` had significant performance issue when reading full content of HTTP request for caching key calculation of MD5 hash value. This hash value was excluded from the caching key. - - [Ocelot.Provider.Kubernetes](https://github.com/ThreeMammals/Ocelot/tree/main/src/Ocelot.Provider.Kubernetes): Fixed long lasting breaking change being added in version [15.0.0](https://github.com/ThreeMammals/Ocelot/releases/tag/15.0.0), see commit https://github.com/ThreeMammals/Ocelot/commit/6e5471a714dddb0a3a40fbb97eac2810cee1c78d. The bug persisted for more than 3 years in versions **15.0.0-22.0.1**, being masked multiple times via class renaming! **Special Thanks to @ZisisTsatsas** who once again brought this issue to our attention, and our team finally realized that we had a breaking change and the provider was broken. +Ensure that you avoid using version [23.2.0](https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0). If you are currently on that version, upgrade to version [{0}](https://github.com/ThreeMammals/Ocelot/releases/tag/{0}) by applying this hotfix patch. - - [Ocelot.Provider.Polly](https://github.com/ThreeMammals/Ocelot/tree/main/src/Ocelot.Provider.Polly): A minor changes without feature delivery. We are preparing for a major update to the package in the next release. -
+### Technical info +With version [23.2.0](https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0), particularly if you have overridden certain service classes or implemented custom logic that manipulates placeholders, you may encounter Ocelot app crashes accompanied by the following errors in the log: +``` +One or more errors occurred. (Unable to start Ocelot, errors are: XXX) +``` +where `XXX` are the following validation error messages: +- `UpstreamPathTemplate 'UUU' doesn't contain the same placeholders in DownstreamPathTemplate 'DDD'` +- `DownstreamPathTemplate 'DDD' doesn't contain the same placeholders in UpstreamPathTemplate 'UUU'` -
- Middlewares. Total 8 Ocelot middlewares were updated - - - `AuthenticationMiddleware`: Added new [Multiple Authentication Schemes](https://github.com/ThreeMammals/Ocelot/pull/1870) feature by @MayorSheFF - - `OutputCacheMiddleware`, `RequestIdMiddleware`: Added new [Cache by Header Value](https://github.com/ThreeMammals/Ocelot/pull/1172) by @EngRajabi, and redesigned as [Default CacheKeyGenerator](https://github.com/ThreeMammals/Ocelot/pull/1849) feature by @raman-m - - `DownstreamUrlCreatorMiddleware`: Fixed [bug](https://github.com/ThreeMammals/Ocelot/issues/748) for ending/omitting slash in path templates aka [Empty placeholders](https://github.com/ThreeMammals/Ocelot/pull/1911) feature by @AlyHKafoury - - `ConfigurationMiddleware`, `HttpRequesterMiddleware`, `ResponderMiddleware`: System upgrade for [Custom HttpMessageInvoker pooling](https://github.com/ThreeMammals/Ocelot/pull/1824) feature by @ggnaegi - - `DownstreamRequestInitialiserMiddleware`: System upgrade for [Performance of Request Mapper](https://github.com/ThreeMammals/Ocelot/pull/1724) feature by @ggnaegi -
+**Finally**, the [validation rules](https://github.com/ThreeMammals/Ocelot/blob/23.2.0/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs#L45-L50) resulted from the incorrect assumption that placeholders are always explicit and can be validated early. Therefore, custom implementations and feature services in the dependency injection (DI) container, which rely on or manipulate placeholders, should validate the configuration JSON and appropriate options later, directly within their service implementations. -
- Documentation for Authentication, Caching, Kubernetes and Routing - - - [Authentication](https://ocelot.readthedocs.io/en/latest/features/authentication.html) - - [Caching](https://ocelot.readthedocs.io/en/latest/features/caching.html) - - [Kubernetes](https://ocelot.readthedocs.io/en/latest/features/kubernetes.html) - - [Routing](https://ocelot.readthedocs.io/en/latest/features/routing.html) -
- -
- Stabilization aka bug fixing - - - See [all bugs](https://github.com/ThreeMammals/Ocelot/issues?q=is%3Aissue+milestone%3ANov-December%2723+is%3Aclosed+label%3Abug) of the [Nov-December'23](https://github.com/ThreeMammals/Ocelot/milestone/2) milestone -
- -
- Testing - - - The `Ocelot.Benchmarks` testing project has been updated with new `PayloadBenchmarks` and `ResponseBenchmarks` by @ggnaegi - - The `Ocelot.AcceptanceTests` testing project has been refactored by @raman-m using the new `AuthenticationSteps` class, and more refactoring will be done in future releases -
- -### Roadmap -We would like to share our team's plans for the future regarding: development trends, ideas, community expectations, etc. -- **Code Review and Performance Improvements**. Without a doubt, we care about code quality every day, following best development practices. And we review, test, refactor, and redesign features with overall performance in mind. In the next few releases (versions 23.x-24.0) we will take care of: generic providers, multiplexing middleware (Aggregation feature), memory management. -- **Server-Sent Events protocol support**. There is a lot of community interest in this HTTP-based protocol. -- **Long Polling for Consul provider**. [Consul](https://www.consul.io/) is our leading technology for service discovery. We are constantly improving the use cases for the `Ocelot.Provider.Consul` package and trying to improve the code inside the package. -- **QoS feature refactoring**. [Polly](https://github.com/App-vNext/Polly/) was released with the new v.8.2+ after .NET 8. So we have to update `Ocelot.Provider.Polly` package taking into account new Polly behavior of redesigned features. -- **Brainstorming** to redesign Rate Limiting, Websockets. More details in future release notes. -- **Planning** of support for Swagger and gRPC proto. More details in future release notes. +### Bug Artifacts +- Released in version: [23.2.0](https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0) +- Introduced in: PR #1927 +- Reported bug: #2031 by @ggnaegi and tested by @Fabman08 +- Hotfix PR: #2032 by @raman-m diff --git a/build.cake b/build.cake index 2b1c998283..a8f7cc362f 100644 --- a/build.cake +++ b/build.cake @@ -1,9 +1,9 @@ -#tool "dotnet:?package=GitVersion.Tool&version=5.8.1" -#tool "dotnet:?package=coveralls.net&version=4.0.1" -#addin nuget:?package=Newtonsoft.Json -#addin nuget:?package=System.Text.Encodings.Web&version=4.7.1 -#tool "nuget:?package=ReportGenerator&version=5.2.0" -#addin Cake.Coveralls&version=1.1.0 +#tool dotnet:?package=GitVersion.Tool&version=5.12.0 // 6.0.0-beta.7 supports .NET 8, 7, 6 +#tool dotnet:?package=coveralls.net&version=4.0.1 +#tool nuget:?package=ReportGenerator&version=5.2.4 +#addin nuget:?package=Newtonsoft.Json&version=13.0.3 +#addin nuget:?package=System.Text.Encodings.Web&version=8.0.0 +#addin nuget:?package=Cake.Coveralls&version=1.1.0 #r "Spectre.Console" using Spectre.Console @@ -13,10 +13,8 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; -// compile -var compileConfig = Argument("configuration", "Release"); - -var slnFile = "./Ocelot.sln"; +const string Release = "Release"; // task name, target, and Release config name +var compileConfig = Argument("configuration", Release); // compile // build artifacts var artifactsDir = Directory("artifacts"); @@ -56,13 +54,15 @@ var nugetFeedStableSymbolsUploadUrl = "https://www.nuget.org/api/v2/package"; string committedVersion = "0.0.0-dev"; GitVersion versioning = null; int releaseId = 0; +bool IsTechnicalRelease = false; string gitHubUsername = "TomPallister"; string gitHubPassword = Environment.GetEnvironmentVariable("OCELOT_GITHUB_API_KEY"); var target = Argument("target", "Default"); - -Information("target is " + target); -Information("Build configuration is " + compileConfig); +var slnFile = (target == Release) ? $"./Ocelot.{Release}.sln" : "./Ocelot.sln"; +Information("\nTarget: " + target); +Information("Build: " + compileConfig); +Information("Solution: " + slnFile); TaskTeardown(context => { AnsiConsole.Markup($"[green]DONE[/] {context.Task.Name}\n"); @@ -82,7 +82,7 @@ Task("RunTests") .IsDependentOn("RunAcceptanceTests") .IsDependentOn("RunIntegrationTests"); -Task("Release") +Task(Release) .IsDependentOn("Build") .IsDependentOn("CreateReleaseNotes") .IsDependentOn("CreateArtifacts") @@ -94,11 +94,18 @@ Task("Compile") .IsDependentOn("Version") .Does(() => { + Information("Build: " + compileConfig); + Information("Solution: " + slnFile); var settings = new DotNetBuildSettings { Configuration = compileConfig, }; - + if (target != Release) + { + settings.Framework = "net8.0"; // build using .NET 8 SDK only + } + Information($"Settings {nameof(DotNetBuildSettings.Framework)}: {settings.Framework}"); + Information($"Settings {nameof(DotNetBuildSettings.Configuration)}: {settings.Configuration}"); DotNetBuild(slnFile, settings); }); @@ -154,12 +161,18 @@ Task("CreateReleaseNotes") var lastReleaseTags = GitHelper("describe --tags --abbrev=0 --exclude net*"); var lastRelease = lastReleaseTags.First(t => !t.StartsWith("net")); // skip 'net*-vX.Y.Z' tag and take 'major.minor.build' - Information("Last release tag is " + lastRelease); - var releaseVersion = versioning.NuGetVersion; + // Read main header from Git file, substitute version in header, and add content further... + Information("{0} New release tag is " + releaseVersion); + Information("{1} Last release tag is " + lastRelease); var releaseHeader = string.Format(System.IO.File.ReadAllText("./ReleaseNotes.md"), releaseVersion, lastRelease); releaseNotes = new List { releaseHeader }; + if (IsTechnicalRelease) + { + WriteReleaseNotes(); + return; + } var shortlogSummary = GitHelper($"shortlog --no-merges --numbered --summary {lastRelease}..HEAD") .ToList(); @@ -298,11 +311,12 @@ Task("CreateReleaseNotes") } } } // END of Top 3 - releaseNotes.Add("### Honoring :medal_sports: aka Top Contributors :clap:"); - releaseNotes.AddRange(topContributors); - releaseNotes.Add(""); - releaseNotes.Add("### Starring :star: aka Release Influencers :bowtie:"); - releaseNotes.AddRange(starring); + + // releaseNotes.Add("### Honoring :medal_sports: aka Top Contributors :clap:"); + // releaseNotes.AddRange(topContributors); + // releaseNotes.Add(""); + // releaseNotes.Add("### Starring :star: aka Release Influencers :bowtie:"); + // releaseNotes.AddRange(starring); releaseNotes.Add(""); releaseNotes.Add($"### Features in Release {releaseVersion}"); var commitsHistory = GitHelper($"log --no-merges --date=format:\"%A, %B %d at %H:%M\" --pretty=format:\"%h by **%aN** on %ad →%n%s\" {lastRelease}..HEAD"); @@ -336,15 +350,23 @@ Task("RunUnitTests") { Configuration = compileConfig, ResultsDirectory = artifactsForUnitTestsDir, - ArgumentCustomization = args => args - // this create the code coverage report - .Append("--collect:\"XPlat Code Coverage\"") + ArgumentCustomization = args => args + .Append("--no-restore") + .Append("--no-build") + .Append("--collect:\"XPlat Code Coverage\"") // this create the code coverage report + .Append("--verbosity:detailed") + .Append("--consoleLoggerParameters:ErrorsOnly") }; - + if (target != Release) + { + testSettings.Framework = "net8.0"; // .NET 8 SDK only + } EnsureDirectoryExists(artifactsForUnitTestsDir); DotNetTest(unitTestAssemblies, testSettings); - var coverageSummaryFile = GetSubDirectories(artifactsForUnitTestsDir).First().CombineWithFilePath(File("coverage.cobertura.xml")); + var coverageSummaryFile = GetSubDirectories(artifactsForUnitTestsDir) + .First() + .CombineWithFilePath(File("coverage.cobertura.xml")); Information(coverageSummaryFile); Information(artifactsForUnitTestsDir); @@ -388,11 +410,15 @@ Task("RunAcceptanceTests") var settings = new DotNetTestSettings { Configuration = compileConfig, + Framework = "net8.0", // .NET 8 SDK only ArgumentCustomization = args => args .Append("--no-restore") .Append("--no-build") }; - + if (target != Release) + { + settings.Framework = "net8.0"; // .NET 8 SDK only + } EnsureDirectoryExists(artifactsForAcceptanceTestsDir); DotNetTest(acceptanceTestAssemblies, settings); }); @@ -404,11 +430,15 @@ Task("RunIntegrationTests") var settings = new DotNetTestSettings { Configuration = compileConfig, + Framework = "net8.0", // .NET 8 SDK only ArgumentCustomization = args => args .Append("--no-restore") .Append("--no-build") }; - + if (target != Release) + { + settings.Framework = "net8.0"; // .NET 8 SDK only + } EnsureDirectoryExists(artifactsForIntegrationTestsDir); DotNetTest(integrationTestAssemblies, settings); }); @@ -416,19 +446,22 @@ Task("RunIntegrationTests") Task("CreateArtifacts") .IsDependentOn("CreateReleaseNotes") .IsDependentOn("Compile") - .Does(() => + .Does(() => { WriteReleaseNotes(); System.IO.File.AppendAllLines(artifactsFile, new[] { "ReleaseNotes.md" }); - CopyFiles("./src/**/Release/Ocelot.*.nupkg", packagesDir); - var projectFiles = GetFiles("./src/**/Release/Ocelot.*.nupkg"); - foreach(var projectFile in projectFiles) + if (!IsTechnicalRelease) { - System.IO.File.AppendAllLines( - artifactsFile, - new[] { projectFile.GetFilename().FullPath } - ); + CopyFiles("./src/**/Release/Ocelot.*.nupkg", packagesDir); + var projectFiles = GetFiles("./src/**/Release/Ocelot.*.nupkg"); + foreach(var projectFile in projectFiles) + { + System.IO.File.AppendAllLines( + artifactsFile, + new[] { projectFile.GetFilename().FullPath } + ); + } } var artifacts = System.IO.File.ReadAllLines(artifactsFile) @@ -511,13 +544,20 @@ Task("PublishToNuget") .IsDependentOn("DownloadGitHubReleaseArtifacts") .Does(() => { - Information("Skipping of publishing to NuGet..."); + if (IsTechnicalRelease) + { + Information("Skipping of publishing to NuGet because of technical release..."); + return; + } + if (IsRunningOnCircleCI()) { PublishPackages(packagesDir, artifactsFile, nugetFeedStableKey, nugetFeedStableUploadUrl, nugetFeedStableSymbolsUploadUrl); } }); +Task("Void").Does(() => {}); + RunTarget(target); private void GenerateReport(Cake.Core.IO.FilePath coverageSummaryFile) @@ -569,7 +609,7 @@ private void PersistVersion(string committedVersion, string newVersion) /// Publishes code and symbols packages to nuget feed, based on contents of artifacts file private void PublishPackages(ConvertableDirectoryPath packagesDir, ConvertableFilePath artifactsFile, string feedApiKey, string codeFeedUrl, string symbolFeedUrl) { - Information("PublishPackages"); + Information("Publishing to NuGet..."); var artifacts = System.IO.File .ReadAllLines(artifactsFile) .Distinct(); @@ -582,17 +622,13 @@ private void PublishPackages(ConvertableDirectoryPath packagesDir, ConvertableFi } var codePackage = packagesDir + File(artifact); + Information("Pushing package " + codePackage + "..."); - Information("Pushing package " + codePackage); - - Information("Calling NuGetPush"); - + Information("Calling DotNetNuGetPush"); DotNetNuGetPush( codePackage, - new DotNetNuGetPushSettings { - ApiKey = feedApiKey, - Source = codeFeedUrl - }); + new DotNetNuGetPushSettings { ApiKey = feedApiKey, Source = codeFeedUrl } + ); } } diff --git a/docker/8.21.0/Dockerfile.base b/docker/8.21.0/Dockerfile.base new file mode 100644 index 0000000000..49b877c101 --- /dev/null +++ b/docker/8.21.0/Dockerfile.base @@ -0,0 +1,16 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine + +RUN apk add bash icu-libs krb5-libs libgcc libintl libssl1.1 libstdc++ zlib git openssh-client + +RUN curl -L --output ./dotnet-install.sh https://dot.net/v1/dotnet-install.sh + +RUN chmod u+x ./dotnet-install.sh + +# Install .NET 8 SDK (already included in the base image, but listed for consistency) +RUN ./dotnet-install.sh -c 8.0 -i /usr/share/dotnet + +# Install .NET 7 SDK +RUN ./dotnet-install.sh -c 7.0 -i /usr/share/dotnet + +# Install .NET 6 SDK +RUN ./dotnet-install.sh -c 6.0 -i /usr/share/dotnet diff --git a/docker/8.21.0/Dockerfile.build b/docker/8.21.0/Dockerfile.build new file mode 100644 index 0000000000..51dff57ff4 --- /dev/null +++ b/docker/8.21.0/Dockerfile.build @@ -0,0 +1,17 @@ +# call from ocelot repo root with +# docker build --platform linux/arm64 --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN -f ./docker/Dockerfile.build . +# docker build --platform linux/amd64 --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN -f ./docker/Dockerfile.build . + +FROM ocelot2/circleci-build:8.21.0 + +ARG OCELOT_COVERALLS_TOKEN + +ENV OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN + +WORKDIR /build + +COPY ./. . + +RUN dotnet tool restore + +RUN dotnet cake diff --git a/docker/8.21.0/Dockerfile.release b/docker/8.21.0/Dockerfile.release new file mode 100644 index 0000000000..90e2d6ee1c --- /dev/null +++ b/docker/8.21.0/Dockerfile.release @@ -0,0 +1,21 @@ +# call from ocelot repo root with +# docker build --platform linux/arm64 --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN --build-arg OCELOT_GITHUB_API_KEY=$OCELOT_GITHUB_API_KEY --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN -f ./docker/Dockerfile.build . +# docker build --platform linux/amd64 --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN --build-arg OCELOT_GITHUB_API_KEY=$OCELOT_GITHUB_API_KEY --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN -f ./docker/Dockerfile.build . + +FROM ocelot2/circleci-build:8.21.0 + +ARG OCELOT_COVERALLS_TOKEN +ARG OCELOT_NUTGET_API_KEY +ARG OCELOT_GITHUB_API_KEY + +ENV OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN +ENV OCELOT_NUTGET_API_KEY=$OCELOT_NUTGET_API_KEY +ENV OCELOT_GITHUB_API_KEY=$OCELOT_GITHUB_API_KEY + +WORKDIR /build + +COPY ./. . + +RUN dotnet tool restore + +RUN dotnet cake diff --git a/docker/8.21.0/build.sh b/docker/8.21.0/build.sh new file mode 100644 index 0000000000..15d1325add --- /dev/null +++ b/docker/8.21.0/build.sh @@ -0,0 +1,11 @@ +# This script builds the Ocelot Docker file + +# {DotNetSdkVer}.{OcelotVer} -> {.NET8}.{21.0} -> 8.21.0 +version=8.21.0 +docker build --platform linux/amd64 -t ocelot2/circleci-build -f Dockerfile.base . + +echo $DOCKER_PASS | docker login -u $DOCKER_USER --password-stdin + +docker tag ocelot2/circleci-build ocelot2/circleci-build:$version +docker push ocelot2/circleci-build:latest +docker push ocelot2/circleci-build:$version diff --git a/docker/Dockerfile.base b/docker/Dockerfile.base index 49b877c101..d2387c9fca 100644 --- a/docker/Dockerfile.base +++ b/docker/Dockerfile.base @@ -1,6 +1,6 @@ FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine -RUN apk add bash icu-libs krb5-libs libgcc libintl libssl1.1 libstdc++ zlib git openssh-client +RUN apk add bash icu-libs krb5-libs libgcc libintl libssl3 libstdc++ zlib git openssh-client RUN curl -L --output ./dotnet-install.sh https://dot.net/v1/dotnet-install.sh @@ -14,3 +14,12 @@ RUN ./dotnet-install.sh -c 7.0 -i /usr/share/dotnet # Install .NET 6 SDK RUN ./dotnet-install.sh -c 6.0 -i /usr/share/dotnet + +# Generate and export the development certificate +RUN dotnet dev-certs https -ep /certs/cert.pem -p '' && \ + chmod 644 /certs/cert.pem + +ENV ASPNETCORE_URLS="https://+;http://+" +ENV ASPNETCORE_HTTPS_PORT=443 +ENV ASPNETCORE_Kestrel__Certificates__Default__Password="" +ENV ASPNETCORE_Kestrel__Certificates__Default__Path=/certs/cert.pem diff --git a/docker/Dockerfile.build b/docker/Dockerfile.build index 51dff57ff4..39d3107b99 100644 --- a/docker/Dockerfile.build +++ b/docker/Dockerfile.build @@ -2,7 +2,7 @@ # docker build --platform linux/arm64 --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN -f ./docker/Dockerfile.build . # docker build --platform linux/amd64 --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN -f ./docker/Dockerfile.build . -FROM ocelot2/circleci-build:8.21.0 +FROM ocelot2/circleci-build:latest ARG OCELOT_COVERALLS_TOKEN diff --git a/docker/Dockerfile.release b/docker/Dockerfile.release index 90e2d6ee1c..89c1b3f799 100644 --- a/docker/Dockerfile.release +++ b/docker/Dockerfile.release @@ -2,7 +2,7 @@ # docker build --platform linux/arm64 --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN --build-arg OCELOT_GITHUB_API_KEY=$OCELOT_GITHUB_API_KEY --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN -f ./docker/Dockerfile.build . # docker build --platform linux/amd64 --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN --build-arg OCELOT_GITHUB_API_KEY=$OCELOT_GITHUB_API_KEY --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN -f ./docker/Dockerfile.build . -FROM ocelot2/circleci-build:8.21.0 +FROM ocelot2/circleci-build:latest ARG OCELOT_COVERALLS_TOKEN ARG OCELOT_NUTGET_API_KEY diff --git a/docker/README.md b/docker/README.md index dcecc224d3..cc1e409bdc 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,3 +1,14 @@ # docker build This folder contains the `Dockerfile.*` and `build.sh` script to create the Ocelot build image & container. + +## Account +- [Ocelot Gateway Profile | Docker Hub](https://hub.docker.com/u/ocelot2) + +## Repositories +- [circleci-build](https://hub.docker.com/r/ocelot2/circleci-build) + +## ocelot2/circleci-build Tags +- [latest](https://hub.docker.com/layers/ocelot2/circleci-build/latest/images/sha256-981d6f9e6e5ba54f6e044bca6fcf8b5197a8f3e6ce2b3cdfa9e6704ecd2ca969?context=explore) is version 8.23.2, uploaded on Apr 3, 2024. +- [8.23.2](https://hub.docker.com/layers/ocelot2/circleci-build/8.23.2/images/sha256-981d6f9e6e5ba54f6e044bca6fcf8b5197a8f3e6ce2b3cdfa9e6704ecd2ca969?context=explore), uploaded on Apr 3, 2024. It supports development SSL certificates by the command `dotnet dev-certs https`. +- [8.21.0](https://hub.docker.com/layers/ocelot2/circleci-build/8.21.0/images/sha256-edb46d37ab52d39a5b27dc63895e5944d4d491d1788744ed144ecb4303b94532?context=explore), uploaded on Nov 20, 2023. It contains .NET 8, 7 and 6 SDKs. It supports builds for `net6.0`, `net7.0` and `net8.0` frameworks. diff --git a/docker/build.sh b/docker/build.sh index 15d1325add..54f16bb966 100755 --- a/docker/build.sh +++ b/docker/build.sh @@ -1,7 +1,7 @@ # This script builds the Ocelot Docker file -# {DotNetSdkVer}.{OcelotVer} -> {.NET8}.{21.0} -> 8.21.0 -version=8.21.0 +# {DotNetSdkVer}.{OcelotVer} -> {.NET8}.{23.2} -> 8.23.2 +version=8.23.2 docker build --platform linux/amd64 -t ocelot2/circleci-build -f Dockerfile.base . echo $DOCKER_PASS | docker login -u $DOCKER_USER --password-stdin diff --git a/docs/_static/overrides.css b/docs/_static/overrides.css new file mode 100644 index 0000000000..a5f45454c8 --- /dev/null +++ b/docs/_static/overrides.css @@ -0,0 +1,6 @@ +blockquote { + font-size: 0.9em; +} +aside.footnote-list { + font-size: 0.9em; +} diff --git a/docs/conf.py b/docs/conf.py index ceea7cca29..6d08568ced 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -8,8 +8,8 @@ project = 'Ocelot' copyright = ' 2016-2024 ThreeMammals Ocelot team' -author = 'Tom Pallister, Ocelot Core team at ThreeMammals' -release = '23.0' +author = 'Tom Pallister, Raman Maksimchuk and Ocelot Core team at ThreeMammals' +release = '23.2' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration @@ -27,3 +27,4 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_static_path html_static_path = ['_static'] +html_css_files = ['overrides.css'] diff --git a/docs/features/administration.rst b/docs/features/administration.rst index cadecefdf9..61832d1ecf 100644 --- a/docs/features/administration.rst +++ b/docs/features/administration.rst @@ -146,4 +146,4 @@ The region is whatever you set against the **Region** field in the `FileCacheOpt """" -.. [#f1] The ``AddOcelot`` method adds default ASP.NET services to DI-container. You could call another more extended ``AddOcelotUsingBuilder`` method while configuring services to build and use custom builder via an ``IMvcCoreBuilder`` interface object. See more instructions in :doc:`../features/dependencyinjection`, "**The AddOcelotUsingBuilder method**" section. +.. [#f1] :ref:`di-the-addocelot-method` adds default ASP.NET services to DI container. You could call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the ":ref:`di-addocelotusingbuilder-method`" section of :doc:`../features/dependencyinjection` feature. diff --git a/docs/features/caching.rst b/docs/features/caching.rst index f2bf302ed1..84803060f6 100644 --- a/docs/features/caching.rst +++ b/docs/features/caching.rst @@ -39,7 +39,8 @@ Finally, in order to use caching on a route in your Route configuration add this "FileCacheOptions": { "TtlSeconds": 15, "Region": "europe-central", - "Header": "Authorization" + "Header": "OC-Caching-Control", + "EnableContentHashing": false // my route has GET verb only, assigning 'true' for requests with body: POST, PUT etc. } In this example **TtlSeconds** is set to 15 which means the cache will expire after 15 seconds. @@ -48,10 +49,38 @@ The **Region** represents a region of caching. Additionally, if a header name is defined in the **Header** property, that header value is looked up by the key (header name) in the ``HttpRequest`` headers, and if the header is found, its value will be included in caching key. This causes the cache to become invalid due to the header value changing. +``EnableContentHashing`` option +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In version `23.0`_, the new property **EnableContentHashing** has been introduced. Previously, the request body was utilized to compute the cache key. +However, due to potential performance issues arising from request body hashing, it has been disabled by default. +Clearly, this constitutes a breaking change and presents challenges for users who require cache key calculations that consider the request body (e.g., for the POST method). +To address this issue, it is recommended to enable the option either at the route level or globally in the :ref:`cch-global-configuration` section: + +.. code-block:: json + + "CacheOptions": { + // ... + "EnableContentHashing": true + } + +.. _cch-global-configuration: + +Global Configuration +-------------------- + +The positive update is that copying Route-level properties for each route is no longer necessary, as version `23.3`_ allows for setting their values in the ``GlobalConfiguration``. +This convenience extends to **Header** and **Region** as well. +However, an alternative is still being sought for **TtlSeconds**, which must be explicitly set for each route to enable caching. + +Notes +----- + If you look at the example `here `_ you can see how the cache manager is setup and then passed into the Ocelot ``AddCacheManager`` configuration method. You can use any settings supported by the **CacheManager** package and just pass them in. -Anyway, Ocelot currently supports caching on the URL of the downstream service and setting a TTL in seconds to expire the cache. You can also clear the cache for a region by calling Ocelot's administration API. +Anyway, Ocelot currently supports caching on the URL of the downstream service and setting a TTL in seconds to expire the cache. +You can also clear the cache for a region by calling Ocelot's administration API. Your Own Caching ---------------- @@ -68,3 +97,6 @@ If you want to add your own caching method, implement the following interfaces a Please dig into the Ocelot source code to find more. We would really appreciate it if anyone wants to implement `Redis `_, `Memcached `_ etc. Please, open a new `Show and tell `_ thread in `Discussions `_ space of the repository. + +.. _23.0: https://github.com/ThreeMammals/Ocelot/releases/tag/23.0.0 +.. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0 diff --git a/docs/features/configuration.rst b/docs/features/configuration.rst index 936a78a9d5..931e4f58d0 100644 --- a/docs/features/configuration.rst +++ b/docs/features/configuration.rst @@ -1,7 +1,7 @@ Configuration ============= -An example configuration can be found here in `ocelot.json `_. +An example configuration can be found here in `ocelot.json`_. There are two sections to the configuration: an array of **Routes** and a **GlobalConfiguration**: * The **Routes** are the objects that tell Ocelot how to treat an upstream request. @@ -75,71 +75,141 @@ More information on how to use these options is below. Multiple Environments --------------------- -Like any other ASP.NET Core project Ocelot supports configuration file names such as **configuration.dev.json**, **configuration.test.json** etc. In order to implement this add the following -to you: +Like any other ASP.NET Core project Ocelot supports configuration file names such as ``appsettings.dev.json``, ``appsettings.test.json`` etc. +In order to implement this add the following to you: .. code-block:: csharp - ConfigureAppConfiguration((hostingContext, config) => + ConfigureAppConfiguration((context, config) => { - config - .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + var env = context.HostingEnvironment; + config.SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) - .AddJsonFile("ocelot.json") - .AddJsonFile($"configuration.{hostingContext.HostingEnvironment.EnvironmentName}.json") + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, true) + .AddJsonFile("ocelot.json") // primary config file + .AddJsonFile($"ocelot.{env.EnvironmentName}.json") // environment file .AddEnvironmentVariables(); }) -Ocelot will now use the environment specific configuration and fall back to **ocelot.json** if there isn't one. +Ocelot will now use the environment specific configuration and fall back to `ocelot.json`_ if there isn't one. You also need to set the corresponding environment variable which is ``ASPNETCORE_ENVIRONMENT``. More info on this can be found in the ASP.NET Core docs: `Use multiple environments in ASP.NET Core `_. +.. _config-merging-files: + Merging Configuration Files --------------------------- -This feature was requested in `issue 296 `_ and allows users to have multiple configuration files to make managing large configurations easier. +This feature allows users to have multiple configuration files to make managing large configurations easier. [#f1]_ -Instead of adding the configuration directly e.g. ``AddJsonFile("ocelot.json")`` you can call ``AddOcelot()`` like below: +Rather than directly adding the configuration e.g., using ``AddJsonFile("ocelot.json")``, you can achieve the same result by invoking ``AddOcelot()`` as shown below: .. code-block:: csharp - ConfigureAppConfiguration((hostingContext, config) => + ConfigureAppConfiguration((context, config) => { - config - .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + var env = context.HostingEnvironment; + config.SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) - .AddOcelot(hostingContext.HostingEnvironment) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, true) + .AddOcelot(env) // happy path .AddEnvironmentVariables(); }) -In this scenario Ocelot will look for any files that match the pattern ``(?i)ocelot.([a-zA-Z0-9]*).json`` and then merge these together. -If you want to set the **GlobalConfiguration** property, you must have a file called **ocelot.global.json**. +In this scenario Ocelot will look for any files that match the pattern ``^ocelot\.(.*?)\.json$`` and then merge these together. +If you want to set the **GlobalConfiguration** property, you must have a file called ``ocelot.global.json``. + +The way Ocelot merges the files is basically load them, loop over them, add any **Routes**, add any **AggregateRoutes** and if the file is called ``ocelot.global.json`` add the **GlobalConfiguration** aswell as any **Routes** or **AggregateRoutes**. +Ocelot will then save the merged configuration to a file called `ocelot.json`_ and this will be used as the source of truth while Ocelot is running. -The way Ocelot merges the files is basically load them, loop over them, add any Routes, add any **AggregateRoutes** and if the file is called **ocelot.global.json** add the **GlobalConfiguration** aswell as any Routes or **AggregateRoutes**. -Ocelot will then save the merged configuration to a file called **ocelot.json** and this will be used as the source of truth while Ocelot is running. + **Note 1**: Currently, validation occurs only during the final merging of configurations in Ocelot. + It's essential to be aware of this when troubleshooting issues. + We recommend thoroughly inspecting the contents of the ``ocelot.json`` file if you encounter any problems. -At the moment there is no validation at this stage it only happens when Ocelot validates the final merged configuration. -This is something to be aware of when you are investigating problems. -We would advise always checking what is in **ocelot.json** file if you have any problems. + **Note 2**: The Merging feature is operational only during the application's startup. + Consequently, the merged configuration in ``ocelot.json`` remains static post-merging and startup. + It's important to be aware that the ``ConfigureAppConfiguration`` method is invoked solely during the startup of an ASP.NET web application. + Once the Ocelot application has started, you cannot call the ``AddOcelot`` method, nor can you employ the merging feature within ``AddOcelot``. + If you still require on-the-fly updating of the primary configuration file, ``ocelot.json``, please refer to the :ref:`config-react-to-changes` section. + Additionally, note that merging partial configuration files (such as ``ocelot.*.json``) on the fly using :doc:`../features/administration` API is not currently implemented. + +Keep files in a folder +^^^^^^^^^^^^^^^^^^^^^^ You can also give Ocelot a specific path to look in for the configuration files like below: .. code-block:: csharp - ConfigureAppConfiguration((hostingContext, config) => + ConfigureAppConfiguration((context, config) => { - config - .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + var env = context.HostingEnvironment; + config.SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) - .AddOcelot("/foo/bar", hostingContext.HostingEnvironment) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, true) + .AddOcelot("/my/folder", env) // happy path .AddEnvironmentVariables(); }) -Ocelot needs the ``HostingEnvironment`` so it knows to exclude anything environment specific from the algorithm. +Ocelot needs the ``HostingEnvironment`` so it knows to exclude anything environment specific from the merging algorithm. + +.. _config-merging-tomemory: + +Merging files to memory [#f2]_ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +By default, Ocelot writes the merged configuration to disk as `ocelot.json`_ (the primary configuration file) by adding the file to the ASP.NET configuration provider. + +If your web server lacks write permissions for the configuration folder, you can instruct Ocelot to use the merged configuration directly from memory. +Here's how: + +.. code-block:: csharp + + // It implicitly calls ASP.NET AddJsonStream extension method for IConfigurationBuilder + // config.AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))); + config.AddOcelot(context.HostingEnvironment, MergeOcelotJson.ToMemory); + +This feature proves exceptionally valuable in cloud environments like Azure, AWS, and GCP, especially when the app lacks sufficient write permissions to save files. +Furthermore, within Docker container environments, permissions can be scarce, necessitating substantial DevOps efforts to enable file write operations. +Therefore, save time by leveraging this feature! [#f2]_ + +Reload JSON Config On Change +---------------------------- + +Ocelot supports reloading the JSON configuration file on change. +For instance, the following will recreate Ocelot internal configuration when the `ocelot.json`_ file is updated manually: + +.. code-block:: csharp + + config.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true); // ASP.NET framework version + +Important Note: Starting from version `23.2`_, most :ref:`di-configuration-addocelot` include optional ``bool?`` arguments, specifically ``optional`` and ``reloadOnChange``. +Therefore, you have the flexibility to provide these arguments when invoking the internal ``AddJsonFile`` method during the final configuration step (see `AddOcelotJsonFile `_ implementation): + +.. code-block:: csharp + + config.AddJsonFile(ConfigurationBuilderExtensions.PrimaryConfigFile, optional ?? false, reloadOnChange ?? false); + +As you can see, in versions prior to `23.2`_, the `AddOcelot extension methods `_ did not apply the ``reloadOnChange`` argument because it was set to ``false``. +We recommend using the ``AddOcelot`` extension methods to control reloading, rather than relying on the framework's ``AddJsonFile`` method. +For example: + +.. code-block:: csharp + + ConfigureAppConfiguration((context, config) => + { + config.AddJsonFile(ConfigurationBuilderExtensions.PrimaryConfigFile, optional: false, reloadOnChange: true); // old approach + var env = context.HostingEnvironment; + var mergeTo = MergeOcelotJson.ToFile; // ToMemory + var folder = "/My/folder"; + FileConfiguration configuration = new(); // read from anywhere and initialize + config.AddOcelot(env, mergeTo, optional: false, reloadOnChange: true); // with environment and merging type + config.AddOcelot(folder, env, mergeTo, optional: false, reloadOnChange: true); // with folder, environment and merging type + config.AddOcelot(configuration, optional: false, reloadOnChange: true); // with configuration object created by your own + config.AddOcelot(configuration, env, mergeTo, optional: false, reloadOnChange: true); // with configuration object, environment and merging type + }) + +Examining the code within the `ConfigurationBuilderExtensions class `_ would be helpful for gaining a better understanding of the signatures of the overloaded methods [#f2]_. Store Configuration in Consul ----------------------------- @@ -151,16 +221,15 @@ The first thing you need to do is install the `NuGet package `_ seconds TTL cache before making a new request to your local Consul agent. +This feature has a `3 seconds `_ TTL cache before making a new request to your local Consul agent. -Reload JSON Config On Change ----------------------------- +.. _config-consul-key: -Ocelot supports reloading the JSON configuration file on change. For instance, the following will recreate Ocelot internal configuration when the **ocelot.json** file is updated manually: - -.. code-block:: csharp - - config.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true); - -Configuration Key ------------------ +Consul Configuration Key [#f4]_ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If you are using Consul for configuration (or other providers in the future), you might want to key your configurations: so you can have multiple configurations. -This feature was requested in `issue 346 `_. + In order to specify the key you need to set the **ConfigurationKey** property in the **ServiceDiscoveryProvider** options of the configuration JSON file e.g. .. code-block:: json @@ -258,7 +320,7 @@ For ``https`` scheme this fake validator was requested by `issue 309 `_. As a team, we do not consider it as an ideal solution. From one side, the community wants to have an option to work with self-signed certificates. -But from other side, currently source code scanners detect 2 serious security vulnerabilities because of this fake validator in `20.0 release `_. +But from other side, currently source code scanners detect 2 serious security vulnerabilities because of this fake validator in `20.0`_ release. The Ocelot team will rethink this unfortunate situation, and it is highly likely that this feature will at least be redesigned or removed completely. For now, the SSL fake validator makes sense in local development environments when a route has ``https`` or ``wss`` schemes having self-signed certificate for those routes. @@ -279,12 +341,14 @@ As a team, we highly recommend following these instructions when developing your * **Production environments**. **Do not use self-signed certificates at all!** System administrators or DevOps engineers must create real valid certificates being signed by hosting or cloud providers. - **Switch off the feature for all routes!** Remove the **DangerousAcceptAnyServerCertificateValidator** property for all routes in production version of **ocelot.json** file! + **Switch off the feature for all routes!** Remove the **DangerousAcceptAnyServerCertificateValidator** property for all routes in production version of `ocelot.json`_ file! + +.. _config-react-to-changes: React to Configuration Changes ------------------------------ -Resolve ``IOcelotConfigurationChangeTokenSource`` interface from the DI container if you wish to react to changes to the Ocelot configuration via the :doc:`../features/administration` API or **ocelot.json** being reloaded from the disk. +Resolve ``IOcelotConfigurationChangeTokenSource`` interface from the DI container if you wish to react to changes to the Ocelot configuration via the :doc:`../features/administration` API or `ocelot.json`_ being reloaded from the disk. You may either poll the change token's ``IChangeToken.HasChanged`` property, or register a callback with the ``RegisterChangeCallback`` method. Polling the HasChanged property @@ -342,6 +406,33 @@ DownstreamHttpVersion Ocelot allows you to choose the HTTP version it will use to make the proxy request. It can be set as ``1.0``, ``1.1`` or ``2.0``. +Dependency Injection +-------------------- + +*Dependency Injection* for this **Configuration** feature in Ocelot is designed to extend and/or control **the configuration** of the Ocelot kernel before the stage of building ASP.NET MVC pipeline services. +The primary methods are :ref:`di-configuration-addocelot` within the `ConfigurationBuilderExtensions`_ class, which offers several overloaded versions with corresponding signatures. + +You can utilize these methods in the ``ConfigureAppConfiguration`` method (located in both **Program.cs** and **Startup.cs**) of your ASP.NET MVC gateway app (minimal web app) to configure the Ocelot pipeline and services. + +.. code-block:: csharp + + namespace Microsoft.AspNetCore.Hosting; + + public interface IWebHostBuilder + { + IWebHostBuilder ConfigureAppConfiguration(Action configureDelegate); + } + +You can find additional details in the dedicated :ref:`di-configuration-overview` section and in subsequent sections related to the :doc:`../features/dependencyinjection` chapter. + """" -.. [#f1] The ``AddOcelot`` method adds default ASP.NET services to DI-container. You could call another more extended ``AddOcelotUsingBuilder`` method while configuring services to build and use custom builder via an ``IMvcCoreBuilder`` interface object. See more instructions in :doc:`../features/dependencyinjection`, "**The AddOcelotUsingBuilder method**" section. +.. [#f1] ":ref:`config-merging-files`" feature was requested in `issue 296 `_, since then we extended it in `issue 1216 `_ (PR `1227 `_) as ":ref:`config-merging-tomemory`" subfeature which was released as a part of version `23.2`_. +.. [#f2] ":ref:`config-merging-tomemory`" subfeature is based on the ``MergeOcelotJson`` enumeration type with values: ``ToFile`` and ``ToMemory``. The 1st one is implicit by default, and the second one is exactly what you need when merging to memory. See more details on implementations in the `ConfigurationBuilderExtensions`_ class. +.. [#f3] :ref:`di-the-addocelot-method` adds default ASP.NET services to DI container. You could call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the ":ref:`di-addocelotusingbuilder-method`" section of :doc:`../features/dependencyinjection` feature. +.. [#f4] ":ref:`config-consul-key`" feature was requested in `issue 346 `_ as a part of version `7.0.0 `_. + +.. _20.0: https://github.com/ThreeMammals/Ocelot/releases/tag/20.0.0 +.. _23.2: https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0 +.. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/test/Ocelot.ManualTest/ocelot.json +.. _ConfigurationBuilderExtensions: https://github.com/ThreeMammals/Ocelot/blob/develop/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs diff --git a/docs/features/dependencyinjection.rst b/docs/features/dependencyinjection.rst index 9dbd9b2be7..dbd3437f66 100644 --- a/docs/features/dependencyinjection.rst +++ b/docs/features/dependencyinjection.rst @@ -1,3 +1,8 @@ +.. _AddOcelot: #the-addocelot-method +.. _AddOcelotUsingBuilder: #addocelotusingbuilder-method +.. _AddDefaultAspNetServices: #adddefaultaspnetservices-method +.. _OcelotBuilder: #ocelotbuilder-class + Dependency Injection ==================== @@ -7,50 +12,77 @@ Dependency Injection Overview -------- -Dependency Injection feature in Ocelot is designed to extend and/or control building of Ocelot core as ASP.NET MVC pipeline services. -The main methods are `AddOcelot <#the-addocelot-method>`_ and `AddOcelotUsingBuilder <#the-addocelotusingbuilder-method>`_ of the ``ServiceCollectionExtensions`` class. -Use them in **Program.cs** and **Startup.cs** of your ASP.NET MVC gateway app (minimal web app) to enable and build Ocelot pipeline. +| Dependency Injection feature in Ocelot is designed to extend and/or control building of Ocelot core as ASP.NET MVC pipeline services. +| The main methods of the `ServiceCollectionExtensions`_ class are: + +* `AddOcelot`_ adds required Ocelot services to DI and it adds default services using `AddDefaultAspNetServices`_ method. +* `AddOcelotUsingBuilder`_ adds required Ocelot services to DI, and **it adds custom ASP.NET services** with configuration injected implicitly or explicitly. + +Use :ref:`di-service-extensions` in in the following ``ConfigureServices`` method (**Program.cs** and **Startup.cs**) of your ASP.NET MVC gateway app (minimal web app) to add/build Ocelot pipeline services: + +.. code-block:: csharp + + namespace Microsoft.AspNetCore.Hosting; + public interface IWebHostBuilder + { + IWebHostBuilder ConfigureServices(Action configureServices); + } + +The fact is, the `OcelotBuilder`_ class is Ocelot's cornerstone logic. + +.. _di-service-extensions: -And of course, the `OcelotBuilder <#the-ocelotbuilder-class>`_ class is the core of Ocelot. +``IServiceCollection`` extensions +--------------------------------- -IServiceCollection extensions ------------------------------ + | **Namespace**: ``Ocelot.DependencyInjection`` + | **Class**: `ServiceCollectionExtensions`_ - **Class**: `Ocelot.DependencyInjection.ServiceCollectionExtensions `_ +Based on the current implementations for the `OcelotBuilder`_ class, the `AddOcelot`_ method adds required ASP.NET services to DI container. +You could call another more extended `AddOcelotUsingBuilder`_ method while configuring services to build and use custom builder via an ``IMvcCoreBuilder`` object. -Based on the current implementations for the ``OcelotBuilder`` class, the ``AddOcelot`` method adds default ASP.NET services to DI container. -You could call another more extended ``AddOcelotUsingBuilder`` method while configuring services to build and use custom builder via an ``IMvcCoreBuilder`` interface object. +.. _di-the-addocelot-method: -The AddOcelot method -^^^^^^^^^^^^^^^^^^^^ +The ``AddOcelot`` method +^^^^^^^^^^^^^^^^^^^^^^^^ **Signatures**: -* ``IOcelotBuilder AddOcelot(this IServiceCollection services)`` -* ``IOcelotBuilder AddOcelot(this IServiceCollection services, IConfiguration configuration)`` +.. code-block:: csharp + + IOcelotBuilder AddOcelot(this IServiceCollection services); + IOcelotBuilder AddOcelot(this IServiceCollection services, IConfiguration configuration); -This ``IServiceCollection`` extension method adds default ASP.NET services and Ocelot application services with configuration injected implicitly or explicitly. -Note! The method adds **default** ASP.NET services required for Ocelot core in the `AddDefaultAspNetServices <#the-adddefaultaspnetservices-method>`_ method which plays the role of default builder. +These ``IServiceCollection`` extension methods add default ASP.NET services and Ocelot application services with configuration injected implicitly or explicitly. -In this scenario, you do nothing except calling the ``AddOcelot`` method which has been mentioned in feature chapters, if additional startup settings are required. -In this case you just reuse default settings to build Ocelot core. The alternative is ``AddOcelotUsingBuilder`` method, see the next section. +**Note!** Both methods add required and **default** ASP.NET services for Ocelot pipeline in the `AddDefaultAspNetServices`_ method which is default builder. -The AddOcelotUsingBuilder method +In this scenario, you do nothing other than call the ``AddOcelot`` method, which is often mentioned in feature chapters, if additional startup settings are required. +With this method, you simply reuse the default settings to build the Ocelot pipeline. The alternative is ``AddOcelotUsingBuilder`` method, see the next subsection. + +.. _di-addocelotusingbuilder-method: + +``AddOcelotUsingBuilder`` method ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Signatures**: -* ``IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, Func customBuilder)`` -* ``IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, IConfiguration configuration, Func customBuilder)`` +.. code-block:: csharp + + using CustomBuilderFunc = System.Func; -This ``IServiceCollection`` extension method adds Ocelot application services, and it *adds custom ASP.NET services* with configuration injected implicitly or explicitly. -Note! The method adds **custom** ASP.NET services required for Ocelot pipeline using custom builder (``customBuilder`` parameter). -It is highly recommended to read docs of the `AddDefaultAspNetServices <#the-adddefaultaspnetservices-method>`_ method, + IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, CustomBuilderFunc customBuilder); + IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, IConfiguration configuration, CustomBuilderFunc customBuilder); + +These ``IServiceCollection`` extension methods add Ocelot application services, and they add **custom ASP.NET services** with configuration injected implicitly or explicitly. + +**Note!** The method adds **custom** ASP.NET services required for Ocelot pipeline using custom builder (aka ``customBuilder`` parameter). +It is highly recommended to read docs of the `AddDefaultAspNetServices`_ method, or even to review implementation to understand default ASP.NET services which are the minimal part of the gateway pipeline. -In this custom scenario, you control everything during ASP.NET MVC pipeline building, and you provide custom settings to build Ocelot core. +In this custom scenario, you control everything during ASP.NET MVC pipeline building, and you provide custom settings to build Ocelot pipeline. -The OcelotBuilder class +``OcelotBuilder`` class ----------------------- **Source code**: `Ocelot.DependencyInjection.OcelotBuilder `_ @@ -58,7 +90,11 @@ The OcelotBuilder class The ``OcelotBuilder`` class is the core of Ocelot which does the following: - Contructs itself by single public constructor: - ``public OcelotBuilder(IServiceCollection services, IConfiguration configurationRoot, Func customBuilder = null)`` + + .. code-block:: csharp + + public OcelotBuilder(IServiceCollection services, IConfiguration configurationRoot, Func customBuilder = null); + - Initializes and stores public properties: **Services** (``IServiceCollection`` object), **Configuration** (``IConfiguration`` object) and **MvcCoreBuilder** (``IMvcCoreBuilder`` object) - Adds **all application services** during construction phase over the ``Services`` property - Adds ASP.NET services by builder using ``Func`` object in these 2 development scenarios: @@ -73,15 +109,15 @@ The ``OcelotBuilder`` class is the core of Ocelot which does the following: * ``AddDelegatingHandler`` method * ``AddConfigPlaceholders`` method -The AddDefaultAspNetServices method +``AddDefaultAspNetServices`` method ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - **Class**: `Ocelot.DependencyInjection.OcelotBuilder `_ + **Class**: `OcelotBuilder`_ Currently the method is protected and overriding is forbidden. -The role of the method is to inject required services via both ``IServiceCollection`` and ``IMvcCoreBuilder`` interfaces objects for the minimal part of the gateway pipeline. +The role of the method is to inject required services via both ``IServiceCollection`` and ``IMvcCoreBuilder`` interface objects for the minimal part of the gateway pipeline. -Current implementation is the folowing: +Current `implementation `_ is the folowing: .. code-block:: csharp @@ -100,9 +136,9 @@ Current implementation is the folowing: } The method cannot be overridden. It is not virtual, and there is no way to override current behavior by inheritance. -And, the method is default builder of Ocelot pipeline while calling the `AddOcelot <#the-addocelot-method>`_ method. +And, the method is default builder of Ocelot pipeline while calling the `AddOcelot`_ method. As alternative, to "override" this default builder, you can design and reuse custom builder as a ``Func`` delegate object -and pass it as parameter to the `AddOcelotUsingBuilder <#the-addocelotusingbuilder-method>`_ extension method. +and pass it as parameter to the `AddOcelotUsingBuilder`_ extension method. It gives you full control on design and buiding of Ocelot pipeline, but be careful while designing your custom Ocelot pipeline as customizable ASP.NET MVC pipeline. Warning! Most of services from minimal part of the pipeline should be reused, but only a few of services could be removed. @@ -116,16 +152,19 @@ Finally, as a default builder, the method above receives ``IMvcCoreBuilder`` obj The next section shows you an example of designing custom Ocelot pipeline by custom builder. +.. _di-custom-builder: + Custom Builder -------------- + **Goal**: Replace ``Newtonsoft.Json`` services with ``System.Text.Json`` services. -The Problem -^^^^^^^^^^^ +Problem +^^^^^^^ -The default `AddOcelot <#the-addocelot-method>`_ method adds +The main `AddOcelot`_ method adds `Newtonsoft JSON `_ services -by the ``AddNewtonsoftJson`` extension method in default builder (the `AddDefaultAspNetServices <#the-adddefaultaspnetservices-method>`_ method). +by the ``AddNewtonsoftJson`` extension method in default builder (`AddDefaultAspNetServices`_ method). The ``AddNewtonsoftJson`` method calling was introduced in old .NET and Ocelot releases which was necessary when Microsoft did not launch the ``System.Text.Json`` library, but now it affects normal use, so we have an intention to solve the problem. @@ -136,12 +175,14 @@ will help to configure JSON settings by the ``JsonSerializerOptions`` property f Solution ^^^^^^^^ -We have the following methods in ``Ocelot.DependencyInjection.ServiceCollectionExtensions`` class: +We have the following methods in `ServiceCollectionExtensions`_ class: -- ``IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, Func customBuilder)`` -- ``IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, IConfiguration configuration, Func customBuilder)`` +.. code-block:: csharp -These method with custom builder allows you to use your any desired JSON library for (de)serialization. + IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, Func customBuilder); + IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, IConfiguration configuration, Func customBuilder); + +These methods with custom builder allow you to use your any desired JSON library for (de)serialization. But we are going to create custom ``MvcCoreBuilder`` with support of JSON services, such as ``System.Text.Json``. To do that we need to call ``AddJsonOptions`` extension of the ``MvcCoreMvcCoreBuilderExtensions`` class (NuGet package: `Microsoft.AspNetCore.Mvc.Core `_) in **Startup.cs**: @@ -181,3 +222,95 @@ To do that we need to call ``AddJsonOptions`` extension of the ``MvcCoreMvcCoreB The sample code provides settings to render JSON as indented text rather than compressed plain JSON text without spaces. This is just one common use case, and you can add additional services to the builder. + +------------------------------------------------------------------ + +.. _di-configuration-overview: + +Configuration Overview +---------------------- + +*Dependency Injection* for the :doc:`../features/configuration` feature in Ocelot is designed to extend and/or control **the configuration** of Ocelot kernel before the stage of building ASP.NET MVC pipeline services. + +To configure the Ocelot pipeline and services, use the :ref:`di-configuration-extensions` in the following ``ConfigureAppConfiguration`` method (located in *Program.cs* and *Startup.cs*) of your minimal web app: + +.. code-block:: csharp + + namespace Microsoft.AspNetCore.Hosting; + public interface IWebHostBuilder + { + IWebHostBuilder ConfigureAppConfiguration(Action configureDelegate); + } + +.. _di-configuration-extensions: + +``IConfigurationBuilder`` extensions +------------------------------------ + + | **Namespace**: ``Ocelot.DependencyInjection`` + | **Class**: `ConfigurationBuilderExtensions`_ + +The main methods are the :ref:`di-configuration-addocelot` within the `ConfigurationBuilderExtensions`_ class. +This method has a list of overloaded versions with corresponding signatures. + +The purpose of this method is to prepare everything before actually configuring with native extensions. It involves the following steps: + +1. **Merging Partial JSON Files**: The ``GetMergedOcelotJson`` method merges partial JSON files. +2. **Selecting Merge Type**: It allows you to choose a merge type to save the merged JSON configuration data either ``ToFile`` or ``ToMemory``. +3. **Framework Extensions**: Finally, the method calls the following native ``IConfigurationBuilder`` framework extensions: + + * The ``AddJsonFile`` method adds the primary configuration file (commonly known as `ocelot.json`_) after the merge stage. It writes the file back **to the file system** using the ``ToFile`` merge type option, which is implicitly the default. + * The ``AddJsonStream`` method adds the JSON data of the primary configuration file as a UTF-8 stream **into memory** after the merge stage. It uses the ``ToMemory`` merge type option. + +.. _di-configuration-addocelot: + +``AddOcelot`` methods +^^^^^^^^^^^^^^^^^^^^^ + +**Signatures** of the most common versions: + +.. code-block:: csharp + + IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, IWebHostEnvironment env); + IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, string folder, IWebHostEnvironment env); + +**Note**: These versions use the implicit ``ToFile`` merge type to write `ocelot.json`_ back to disk. Finally, they call the ``AddJsonFile`` extension. + +**Signatures** of the versions to specify a ``MergeOcelotJson`` option: + +.. code-block:: csharp + + IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, IWebHostEnvironment env, MergeOcelotJson mergeTo, + string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null); + IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, string folder, IWebHostEnvironment env, MergeOcelotJson mergeTo, + string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null); + +**Note**: These versions include optional arguments to specify the location of the three main files involved in the merge operation. +In theory, these files can be located anywhere, but in practice, it is better to keep them in one folder. + +**Signatures** of the versions to indicate the ``FileConfiguration`` object of a self-created out-of-the-box configuration: [#f1]_ + +.. code-block:: csharp + + IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration, + string primaryConfigFile = null, bool? optional = null, bool? reloadOnChange = null); + IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration, IWebHostEnvironment env, MergeOcelotJson mergeTo, + string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null); + +| **Note 1**: These versions include optional arguments to specify the location of the three main files involved in the merge operation. +| **Note 2**: Your ``FileConfiguration`` object can be serialized/deserialized from anywhere: local or remote storage, Consul KV storage, and even a database. + For more information about this super useful feature, please read PR `1569`_ [#f1]_. + +"""" + +.. [#f1] The Dynamic :doc:`../features/configuration` feature was requested in issues `1228`_ and `1235`_. It was delivered by PR `1569`_ as part of version `20.0`_. Since then, we have extended it in PR `1227`_ and released it as part of version `23.2`_. + +.. _ServiceCollectionExtensions: https://github.com/ThreeMammals/Ocelot/blob/develop/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs#L7 +.. _ConfigurationBuilderExtensions: https://github.com/ThreeMammals/Ocelot/blob/develop/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs +.. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/test/Ocelot.ManualTest/ocelot.json +.. _1227: https://github.com/ThreeMammals/Ocelot/pull/1227 +.. _1228: https://github.com/ThreeMammals/Ocelot/issues/1228 +.. _1235: https://github.com/ThreeMammals/Ocelot/issues/1235 +.. _1569: https://github.com/ThreeMammals/Ocelot/pull/1569 +.. _20.0: https://github.com/ThreeMammals/Ocelot/releases/tag/20.0.0 +.. _23.2: https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0 diff --git a/docs/features/kubernetes.rst b/docs/features/kubernetes.rst index 44411e5892..8c6967c879 100644 --- a/docs/features/kubernetes.rst +++ b/docs/features/kubernetes.rst @@ -73,7 +73,11 @@ The example here shows a typical configuration: } Service deployment in **Namespace** ``Dev``, **ServiceDiscoveryProvider** type is ``Kube``, you also can set :ref:`k8s-pollkube-provider` type. -Note: **Host**, **Port** and **Token** are no longer in use. + + **Note 1**: ``Host``, ``Port`` and ``Token`` are no longer in use. + + **Note 2**: The ``Kube`` provider searches for the service entry using ``ServiceName`` and then retrieves the first available port from the ``EndpointSubsetV1.Ports`` collection. + Therefore, if the port name is not specified, the default downstream scheme will be ``http``; .. _k8s-pollkube-provider: @@ -99,10 +103,10 @@ This really depends on how volatile your services are. We doubt it will matter for most people and polling may give a tiny performance improvement over calling Kubernetes per request. There is no way for Ocelot to work these out for you. -Global vs Route levels -^^^^^^^^^^^^^^^^^^^^^^ +Global vs Route Levels +---------------------- -If your downstream service resides in a different namespace, you can override the global setting at the Route-level by specifying a **ServiceNamespace**: +If your downstream service resides in a different namespace, you can override the global setting at the Route-level by specifying a ``ServiceNamespace``: .. code-block:: json @@ -113,7 +117,45 @@ If your downstream service resides in a different namespace, you can override th } ] +Downstream Scheme vs Port Names [#f3]_ +-------------------------------------- + +Kubernetes configuration permits the definition of multiple ports with names for each address of an endpoint subset. +When binding multiple ports, you assign a name to each subset port. +To allow the ``Kube`` provider to recognize the desired port by its name, you need to specify the ``DownstreamScheme`` with the port's name; +if not, the collection's first port entry will be chosen by default. + +For instance, consider a service on Kubernetes that exposes two ports: ``https`` for **443** and ``http`` for **80**, as follows: + +.. code-block:: text + + Name: my-service + Namespace: default + Subsets: + Addresses: 10.1.161.59 + Ports: + Name Port Protocol + ---- ---- -------- + https 443 TCP + http 80 TCP + +**When** you need to use the ``http`` port while intentionally bypassing the default ``https`` port (first one), +you must define ``DownstreamScheme`` to enable the provider to recognize the desired ``http`` port by comparing ``DownstreamScheme`` with the port name as follows: + +.. code-block:: json + + "Routes": [ + { + "ServiceName": "my-service", + "DownstreamScheme": "http", // port name -> http -> port is 80 + } + ] + +**Note**: In the absence of a specified ``DownstreamScheme`` (which is the default behavior), the ``Kube`` provider will select **the first available port** from the ``EndpointSubsetV1.Ports`` collection. +Consequently, if the port name is not designated, the default downstream scheme utilized will be ``http``. + """" .. [#f1] `Wikipedia `_ | `K8s Website `_ | `K8s Documentation `_ | `K8s GitHub `_ .. [#f2] This feature was requested as part of `issue 345 `_ to add support for `Kubernetes `_ :doc:`../features/servicediscovery` provider. +.. [#f3] *"Downstream Scheme vs Port Names"* feature was requested as part of `issue 1967 `_ and released in version `23.3 `_ diff --git a/docs/features/qualityofservice.rst b/docs/features/qualityofservice.rst index 5efab9f1d9..799efdc086 100644 --- a/docs/features/qualityofservice.rst +++ b/docs/features/qualityofservice.rst @@ -1,55 +1,125 @@ -Quality of Service -================== - - Label: `QoS `_ - -Ocelot supports one QoS capability at the current time. You can set on a per Route basis if you want to use a circuit breaker when making requests to a downstream service. -This uses an awesome .NET library called `Polly `_, check them out `in official repo `_. - -The first thing you need to do if you want to use the :doc:`../features/administration` API is bring in the relevant NuGet `package `_: - -.. code-block:: powershell - - Install-Package Ocelot.Provider.Polly - -Then in your ``ConfigureServices`` method to add Polly services we must call the ``AddPolly()`` extension of the ``OcelotBuilder`` being returned by ``AddOcelot()`` [#f1]_ like below: - -.. code-block:: csharp - - services.AddOcelot() - .AddPolly(); - -Then add the following section to a Route configuration: - -.. code-block:: json - - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 1000, - "TimeoutValue": 5000 - } - -- You must set a number greater than ``0`` against **ExceptionsAllowedBeforeBreaking** for this rule to be implemented. [#f2]_ -- **DurationOfBreak** means the circuit breaker will stay open for 1 second after it is tripped. -- **TimeoutValue** means if a request takes more than 5 seconds, it will automatically be timed out. - -You can set the **TimeoutValue** in isolation of the **ExceptionsAllowedBeforeBreaking** and **DurationOfBreak** options: - -.. code-block:: json - - "QoSOptions": { - "TimeoutValue": 5000 - } - -There is no point setting the other two in isolation as they affect each other! - -If you do not add a QoS section, QoS will not be used, however Ocelot will default to a **90** seconds timeout on all downstream requests. -If someone needs this to be configurable, open an issue. [#f2]_ - -"""" - -.. [#f1] The ``AddOcelot`` method adds default ASP.NET services to DI-container. You could call another more extended ``AddOcelotUsingBuilder`` method while configuring services to build and use custom builder via an ``IMvcCoreBuilder`` interface object. See more instructions in :doc:`../features/dependencyinjection`, "**The AddOcelotUsingBuilder method**" section. -.. [#f2] If something doesn't work or you get stuck, please review current `QoS issues `_ filtering by |QoS_label| label. - -.. |QoS_label| image:: https://img.shields.io/badge/-QoS-D3ADAF.svg - :target: https://github.com/ThreeMammals/Ocelot/labels/QoS +Quality of Service +================== + + Label: `QoS `_ + +Ocelot supports one QoS capability at the current time. You can set on a per Route basis if you want to use a circuit breaker when making requests to a downstream service. +This uses an awesome .NET library called `Polly`_, check them out `in official repository `_. + +The first thing you need to do if you want to use the :doc:`../features/administration` API is bring in the relevant NuGet `package `_: + +.. code-block:: powershell + + Install-Package Ocelot.Provider.Polly + +Then in your ``ConfigureServices`` method to add `Polly`_ services we must call the ``AddPolly()`` extension of the ``OcelotBuilder`` being returned by ``AddOcelot()`` [#f1]_ like below: + +.. code-block:: csharp + + services.AddOcelot() + .AddPolly(); + +Then add the following section to a Route configuration: + +.. code-block:: json + + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 1000, + "TimeoutValue": 5000 + } + +- You must set a number equal or greater than ``2`` against **ExceptionsAllowedBeforeBreaking** for this rule to be implemented. [#f2]_ +- **DurationOfBreak** means the circuit breaker will stay open for 1 second after it is tripped. +- **TimeoutValue** means if a request takes more than 5 seconds, it will automatically be timed out. + +You can set the **TimeoutValue** in isolation of the **ExceptionsAllowedBeforeBreaking** and **DurationOfBreak** options: + +.. code-block:: json + + "QoSOptions": { + "TimeoutValue": 5000 + } + +There is no point setting the other two in isolation as they affect each other! + +Defaults +-------- + +If you do not add a QoS section, QoS will not be used, however Ocelot will default to a **90** seconds timeout on all downstream requests. +If someone needs this to be configurable, open an issue. [#f2]_ + +.. _qos-polly-v7-vs-v8: + +`Polly`_ v7 vs v8 +----------------- + +Important changes in version `23.2`_: [#f3]_ + + - With `Polly`_ version 8+, the ``ExceptionsAllowedBeforeBreaking`` value must be equal to or greater than **2**! + - The ``AddPolly`` method has been migrated from v7 policy wrappers to v8 resilience pipelines. Consequently, it now exhibits different behavior based on v8 pipelines. + +If you prefer not to modify your settings, you can continue using `Polly`_ v7 as follows: + +.. code-block:: csharp + + services.AddOcelot() + .AddPollyV7(); + +**Note**: Support for `Polly`_ v7 will be removed in a future version. We recommend avoiding this method (which is tagged as ``Obsolete``) unless absolutely necessary. + +.. _qos-extensibility: + +Extensibility [#f3]_ +-------------------- + +If you want to use your ``ResiliencePipeline`` provider, you can use the following syntax: + +.. code-block:: csharp + + services.AddOcelot() + .AddPolly(); + // MyProvider should implement IPollyQoSResiliencePipelineProvider + // Note: you can use standard provider PollyQoSResiliencePipelineProvider + +If, in addition, you want to use your own ``DelegatingHandler``, you can use the following syntax: + +.. code-block:: csharp + + services.AddOcelot() + .AddPolly(MyQosDelegatingHandlerDelegate); + // MyProvider should implement IPollyQoSResiliencePipelineProvider + // Note: you can use standard provider PollyQoSResiliencePipelineProvider + // MyQosDelegatingHandlerDelegate is a delegate use to get a DelegatingHandler + +And finally, if you want to define your own set of exceptions to map, you can use the following syntax: + +.. code-block:: csharp + + services.AddOcelot() + .AddPolly(MyErrorMapping); + // MyProvider should implement IPollyQoSResiliencePipelineProvider + // Note: you can use standard provider PollyQoSResiliencePipelineProvider + + // MyErrorMapping is a Dictionary>, eg: + private static readonly Dictionary> MyErrorMapping = new() + { + {typeof(TaskCanceledException), CreateError}, + {typeof(TimeoutRejectedException), CreateError}, + {typeof(BrokenCircuitException), CreateError}, + {typeof(BrokenCircuitException), CreateError}, + }; + private static Error CreateError(Exception e) => new RequestTimedOutError(e); + +"""" + +.. [#f1] :ref:`di-the-addocelot-method` adds default ASP.NET services to DI container. You could call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the ":ref:`di-addocelotusingbuilder-method`" section of :doc:`../features/dependencyinjection` feature. +.. [#f2] If something doesn't work or you get stuck, please review current `QoS issues `_ filtering by |QoS_label| label. +.. [#f3] We upgraded `Polly`_ version from v7.x to v8.x! The :ref:`qos-extensibility` feature was requested in issue `1875`_ and delivered by PR `1914`_ as a part of version `23.2`_. + +.. _Polly: https://www.thepollyproject.org +.. _1875: https://github.com/ThreeMammals/Ocelot/issues/1875 +.. _1914: https://github.com/ThreeMammals/Ocelot/pull/1914 +.. _23.2: https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0 +.. |QoS_label| image:: https://img.shields.io/badge/-QoS-D3ADAF.svg + :target: https://github.com/ThreeMammals/Ocelot/labels/QoS diff --git a/docs/features/ratelimiting.rst b/docs/features/ratelimiting.rst index 9a69f4dedf..94db1db5df 100644 --- a/docs/features/ratelimiting.rst +++ b/docs/features/ratelimiting.rst @@ -1,35 +1,50 @@ Rate Limiting ============= +`What's rate limiting? `_ + +* `Rate limiting | Wikipedia `_ +* `Rate Limiting pattern | Azure Architecture Center | Microsoft Learn `_ +* `Rate Limiting | Ask Google `_ + Ocelot Own Implementation ------------------------- -Ocelot supports rate limiting of upstream requests so that your downstream services do not become overloaded. +Ocelot provides *rate limiting* for upstream requests to prevent downstream services from becoming overwhelmed. [#f1]_ -The authors of this feature were inspired by `@catcherwong article `_ to finally write this documentation. -This feature was added by `@geffzhang `_ on GitHub! Thanks very much! +Rate Limit by Client's Header +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -To get rate limiting working for a Route you need to add the following JSON to it: +To implement *rate limiting* for a Route, you need to incorporate the following JSON configuration: .. code-block:: json "RateLimitOptions": { - "ClientWhitelist": [], + "ClientWhitelist": [], // array of strings "EnableRateLimiting": true, - "Period": "1s", - "PeriodTimespan": 1, + "Period": "1s", // seconds, minutes, hours, days + "PeriodTimespan": 1, // only seconds "Limit": 1 } -* **ClientWhitelist** - This is an array that contains the whitelist of the client. - It means that the client in this array will not be affected by the rate limiting. -* **EnableRateLimiting** - This value specifies enable endpoint rate limiting. -* **Period** - This value specifies the period that the limit applies to, such as ``1s``, ``5m``, ``1h``, ``1d`` and so on. - If you make more requests in the period than the limit allows then you need to wait for **PeriodTimespan** to elapse before you make another request. -* **PeriodTimespan** - This value specifies that we can retry after a certain number of seconds. -* **Limit** - This value specifies the maximum number of requests that a client can make in a defined period. +* **ClientWhitelist** - An array containing the whitelisted clients. Clients listed here will be exempt from rate limiting. + For more information on the **ClientIdHeader** option, refer to the :ref:`rl-global-configuration` section. +* **EnableRateLimiting** - This setting enables rate limiting on endpoints. +* **Period** - This parameter defines the duration for which the limit is applicable, such as ``1s`` (seconds), ``5m`` (minutes), ``1h`` (hours), and ``1d`` (days). + If you reach the exact **Limit** of requests, the excess occurs immediately, and the **PeriodTimespan** begins. + You must wait for the **PeriodTimespan** duration to pass before making another request. + Should you exceed the number of requests within the period more than the **Limit** permits, the **QuotaExceededMessage** will appear in the response, accompanied by the **HttpStatusCode**. +* **PeriodTimespan** - This parameter indicates the time in **seconds** after which a retry is permissible. + During this interval, the **QuotaExceededMessage** will appear in the response, accompanied by an **HttpStatusCode**. + Clients are advised to consult the ``Retry-After`` header to determine the timing of subsequent requests. +* **Limit** - This parameter defines the upper limit of requests a client is allowed to make within a specified **Period**. + +.. _rl-global-configuration: + +Global Configuration +^^^^^^^^^^^^^^^^^^^^ -You can also set the following in the **GlobalConfiguration** part of **ocelot.json**: +You can set the following in the ``GlobalConfiguration`` section of `ocelot.json`_: .. code-block:: json @@ -38,33 +53,48 @@ You can also set the following in the **GlobalConfiguration** part of **ocelot.j "RateLimitOptions": { "DisableRateLimitHeaders": false, "QuotaExceededMessage": "Customize Tips!", - "HttpStatusCode": 123, - "ClientIdHeader": "Test" + "HttpStatusCode": 418, // I'm a teapot + "ClientIdHeader": "MyRateLimiting" } } -* **DisableRateLimitHeaders** - This value specifies whether ``X-Rate-Limit`` and ``Retry-After`` headers are disabled. -* **QuotaExceededMessage** - This value specifies the exceeded message. -* **HttpStatusCode** - This value specifies the returned HTTP status code when rate limiting occurs. -* **ClientIdHeader** - Allows you to specifiy the header that should be used to identify clients. By default it is ``ClientId`` +* **DisableRateLimitHeaders** - Determines if the ``X-Rate-Limit`` and ``Retry-After`` headers are disabled. +* **QuotaExceededMessage** - Defines the message displayed when the quota is exceeded. It is optional and the default message is informative. +* **HttpStatusCode** - Indicates the HTTP status code returned during *rate limiting*. The default value is **429** (`Too Many Requests`_). +* **ClientIdHeader** - Specifies the header used to identify clients, with ``ClientId`` as the default. Future and ASP.NET Core Implementation -------------------------------------- -The Ocelot team considers to redesign *Rate Limiting* feature, -because of `Announcing Rate Limiting for .NET `_ by Brennan Conroy on July 13th, 2022. -There is no decision at the moment, and the old version of the feature is included as a part of release `20.0 `_ for .NET 7. +The Ocelot team is contemplating a redesign of the *Rate Limiting* feature following the `Announcing Rate Limiting for .NET`_ by Brennan Conroy on July 13th, 2022. +Currently, no decision has been made, and the previous version of the feature remains part of the `20.0`_ release for .NET 7. [#f2]_ -See more about new feature being added into ASP.NET Core 7.0 release: +Discover the new features being introduced in the ASP.NET Core 7.0 release: -* `RateLimiter Class `_, since ASP.NET Core **7.0** -* `System.Threading.RateLimiting `_ NuGet package -* `Rate limiting middleware in ASP.NET Core `_ article by Arvin Kahbazi, Maarten Balliauw, and Rick Anderson +* The `RateLimiter Class `_, available since ASP.NET Core 7.0 +* The `System.Threading.RateLimiting `_ NuGet package +* The `Rate limiting middleware in ASP.NET Core `_ article by Arvin Kahbazi, Maarten Balliauw, and Rick Anderson -However, it makes sense to keep the old implementation as a Ocelot built-in native feature, but we are going to migrate to the new Rate Limiter from ``Microsoft.AspNetCore.RateLimiting`` namespace. +While retaining the old implementation as an Ocelot built-in feature makes sense, we plan to transition to the new Rate Limiter from the ``Microsoft.AspNetCore.RateLimiting`` namespace. +Please share your thoughts with us in the `Discussions `_ space of the repository. |octocat| + +"""" + +.. [#f1] Historically, the *"Ocelot Own Rate Limiting"* feature is one of the oldest and first features of Ocelot. This feature was delivered in PR `37`_ by `@geffzhang`_ on GitHub. Many thanks! It was initially released in version `1.3.2`_. The authors were inspired by `@catcherwong article`_ to write this documentation. +.. [#f2] Since PR `37`_ and version `1.3.2`_, the Ocelot team has reviewed and redesigned the feature to provide stable behavior. The fix for bug `1590`_ (PR `1592`_) was released as part of version `23.3`_. + +.. _Announcing Rate Limiting for .NET: https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet/ +.. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/test/Ocelot.ManualTest/ocelot.json +.. _@geffzhang: https://github.com/ThreeMammals/Ocelot/commits?author=geffzhang +.. _@catcherwong article: http://www.c-sharpcorner.com/article/building-api-gateway-using-ocelot-in-asp-net-core-rate-limiting-part-four/ +.. _Too Many Requests: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429 +.. _37: https://github.com/ThreeMammals/Ocelot/pull/37 +.. _1590: https://github.com/ThreeMammals/Ocelot/issues/1590 +.. _1592: https://github.com/ThreeMammals/Ocelot/pull/1592 +.. _1.3.2: https://github.com/ThreeMammals/Ocelot/releases/tag/1.3.2 +.. _20.0: https://github.com/ThreeMammals/Ocelot/releases/tag/20.0.0 +.. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0 .. |octocat| image:: https://github.githubassets.com/images/icons/emoji/octocat.png :alt: octocat :width: 23 - -Please, share your opinion to us in the `Discussions `_ space of the repository. |octocat| diff --git a/docs/features/requestaggregation.rst b/docs/features/requestaggregation.rst index a07e7f9821..c9a6423885 100644 --- a/docs/features/requestaggregation.rst +++ b/docs/features/requestaggregation.rst @@ -1,24 +1,105 @@ -Request Aggregation -=================== +Request Aggregation [#f1]_ +========================== Ocelot allows you to specify Aggregate Routes that compose multiple normal Routes and map their responses into one object. This is usually where you have a client that is making multiple requests to a server where it could just be one. -This feature allows you to start implementing back-end for a front-end (BFF) type architecture with Ocelot. - -This feature was requested as part of `issue 79 `_ and further improvements were made as part of `issue 298 `_. +This feature allows you to start implementing back-end for a front-end (BFF) type architecture with Ocelot. [#f1]_ In order to set this up you must do something like the following in your **ocelot.json**. Here we have specified two normal Routes and each one has a **Key** property. We then specify an Aggregate that composes the two Routes using their keys in the **RouteKeys** list and says then we have the **UpstreamPathTemplate** which works like a normal Route. Obviously you cannot have duplicate **UpstreamPathTemplates** between **Routes** and **Aggregates**. -You can use all of Ocelot's normal Route options apart from **RequestIdKey** (explained in `gotchas <#gotchas>`_ below). +You can use all of Ocelot's normal Route options apart from **RequestIdKey** (explained in :ref:`agg-gotchas` below). + +Basic Expecting JSON from Downstream Services +--------------------------------------------- + +.. code-block:: json + + { + "Routes": [ + { + "UpstreamHttpMethod": [ "Get" ], + "UpstreamPathTemplate": "/laura", + "DownstreamPathTemplate": "/", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { "Host": "localhost", "Port": 51881 } + ], + "Key": "Laura" + }, + { + "UpstreamHttpMethod": [ "Get" ], + "UpstreamPathTemplate": "/tom", + "DownstreamPathTemplate": "/", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { "Host": "localhost", "Port": 51882 } + ], + "Key": "Tom" + } + ], + "Aggregates": [ + { + "UpstreamPathTemplate": "/", + "RouteKeys": [ "Tom", "Laura" ] + } + ] + } + +You can also set **UpstreamHost** and **RouteIsCaseSensitive** in the Aggregate configuration. These behave the same as any other Routes. + +If the Route ``/tom`` returned a body of ``{"Age": 19}`` and ``/laura`` returned ``{"Age": 25}``, the the response after aggregation would be as follows: + +.. code-block:: json + + {"Tom":{"Age": 19},"Laura":{"Age": 25}} + +At the moment the aggregation is very simple. Ocelot just gets the response from your downstream service and sticks it into a JSON dictionary as above. +With the Route key being the key of the dictionary and the value the response body from your downstream service. +You can see that the object is just JSON without any pretty spaces etc. + +Note, all headers will be lost from the downstream services response. + +Ocelot will always return content type ``application/json`` with an aggregate request. + +If you downstream services return a `404 Not Found `_, the aggregate will just return nothing for that downstream service. +It will not change the aggregate response into a ``404`` even if all the downstreams return a ``404``. + +Use Complex Aggregation +----------------------- + +Imagine you'd like to use aggregated queries, but you don't know all the parameters of your queries. You first need to call an endpoint to obtain the necessary data, for example a user's id, and then return the user's details. + +Let's say we have an endpoint that returns a series of comments with references to various users or threads. The author of the comments is referenced by his Id, but you'd like to return all the details about the author. + +Here, you could use aggregation to get 1) all the comments, 2) attach the author details. In fact there are 2 endpoints that are called, but for the 2nd, you dynamically replace the user's Id in the route to obtain the details. + +In concrete terms: -Advanced Register Your Own Aggregators --------------------------------------- +1) ``/Comments`` contains the authorId property +2) ``/users/{userId}`` with ``{userId}`` replaced by **authorId** to obtain the user's details. + +This functionality is still in its early stages, but it does allow you to search for data based on an initial request. + +To perform the mapping, you need to use **AggregateRouteConfig**: + +.. code-block:: csharp + + new AggregateRouteConfig + { + RouteKey = "UserDetails", + JsonPath = "$[*].authorId", + Parameter = "userId" + }; + +**RouteKey** is used as a reference for the route, **JsonPath** indicates where the parameter you are interested in is located in the first request response body and **Parameter** tells us that the value for ``authorId`` should be used for the request parameter ``userId``. + +Register Your Own Aggregators +----------------------------- Ocelot started with just the basic request aggregation and since then we have added a more advanced method that let's the user take in the responses from the downstream services and then aggregate them into a response object. - The **ocelot.json** setup is pretty much the same as the basic aggregation approach apart from you need to add an **Aggregator** property like below: .. code-block:: json @@ -60,7 +141,7 @@ The **ocelot.json** setup is pretty much the same as the basic aggregation appro Here we have added an aggregator called ``FakeDefinedAggregator``. Ocelot is going to look for this aggregator when it tries to aggregate this Route. -In order to make the aggregator available we must add the ``FakeDefinedAggregator`` to the ``OcelotBuilder`` being returned by ``AddOcelot()`` [#f1]_ like below: +In order to make the aggregator available we must add the ``FakeDefinedAggregator`` to the ``OcelotBuilder`` being returned by ``AddOcelot()`` [#f2]_ like below: .. code-block:: csharp @@ -98,74 +179,52 @@ In order to make an Aggregator you must implement this interface: } With this feature you can pretty much do whatever you want because the ``HttpContext`` objects contain the results of all the aggregate requests. -Please note, if the ``HttpClient`` throws an exception when making a request to a Route in the aggregate then you will not get a ``HttpContext`` for it, but you would for any that succeed. -If it does throw an exception, this will be logged. -Basic Expecting JSON from Downstream Services ---------------------------------------------- +Please note, if the ``HttpClient`` throws an exception when making a request to a Route in the aggregate then you will not get a ``HttpContext`` for it, but you would for any that succeed. If it does throw an exception, this will be logged. -.. code-block:: json +Below is an example of an aggregator that you could implement for your solution: +.. code-block:: csharp + + public class FakeDefinedAggregator : IDefinedAggregator { - "Routes": [ - { - "UpstreamHttpMethod": [ "Get" ], - "UpstreamPathTemplate": "/laura", - "DownstreamPathTemplate": "/", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { "Host": "localhost", "Port": 51881 } - ], - "Key": "Laura" - }, - { - "UpstreamHttpMethod": [ "Get" ], - "UpstreamPathTemplate": "/tom", - "DownstreamPathTemplate": "/", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { "Host": "localhost", "Port": 51882 } - ], - "Key": "Tom" - } - ], - "Aggregates": [ + public async Task Aggregate(List responseHttpContexts) { - "UpstreamPathTemplate": "/", - "RouteKeys": [ - "Tom", - "Laura" - ] + // The aggregator gets a list of downstream responses as parameter. + // You can now implement your own logic to aggregate the responses (including bodies and headers) from the downstream services + var responses = responseHttpContexts.Select(x => x.Items.DownstreamResponse()).ToArray(); + + // In this example we are concatenating the results, + // but you could create a more complex construct, up to you. + var contentList = new List(); + foreach (var response in responses) + { + var content = await response.Content.ReadAsStringAsync(); + contentList.Add(content); + } + + // The only constraint here: You must return a DownstreamResponse object. + return new DownstreamResponse( + new StringContent(JsonConvert.SerializeObject(contentList)), + HttpStatusCode.OK, + responses.SelectMany(x => x.Headers).ToList(), + "reason"); } - ] } -You can also set **UpstreamHost** and **RouteIsCaseSensitive** in the Aggregate configuration. These behave the same as any other Routes. - -If the Route ``/tom`` returned a body of ``{"Age": 19}`` and ``/laura`` returned ``{"Age": 25}``, the the response after aggregation would be as follows: - -.. code-block:: json - - {"Tom":{"Age": 19},"Laura":{"Age": 25}} - -At the moment the aggregation is very simple. Ocelot just gets the response from your downstream service and sticks it into a JSON dictionary as above. -With the Route key being the key of the dictionary and the value the response body from your downstream service. -You can see that the object is just JSON without any pretty spaces etc. - -Note, all headers will be lost from the downstream services response. - -Ocelot will always return content type ``application/json`` with an aggregate request. - -If you downstream services return a `404 Not Found `_, the aggregate will just return nothing for that downstream service. -It will not change the aggregate response into a ``404`` even if all the downstreams return a ``404``. +.. _agg-gotchas: Gotchas ------- -You cannot use Routes with specific **RequestIdKeys** as this would be crazy complicated to track. +* You cannot use Routes with specific **RequestIdKeys** as this would be crazy complicated to track. +* Aggregation only supports the ``GET`` HTTP verb. +* Aggregation allows for the forwarding of ``HttpRequest.Body`` to downstream services by duplicating the body data. + Form data and attached files should also be forwarded. + It is essential to always specify the ``Content-Length`` header in requests to upstream; otherwise, Ocelot will log warnings like *"Aggregation does not support body copy without Content-Length header!"*. -Aggregation only supports the ``GET`` HTTP verb. """" -.. [#f1] The ``AddOcelot`` method adds default ASP.NET services to DI-container. You could call another more extended ``AddOcelotUsingBuilder`` method while configuring services to build and use custom builder via an ``IMvcCoreBuilder`` interface object. See more instructions in :doc:`../features/dependencyinjection`, "**The AddOcelotUsingBuilder method**" section. +.. [#f1] This feature was requested as part of `issue 79 `_ and further improvements were made as part of `issue 298 `_. A significant refactoring and revision of the `Multiplexer `_ design was carried out on March 4, 2024 in version `23.1 `_, see PRs `1826 `_ and `1462 `_. +.. [#f2] :ref:`di-the-addocelot-method` adds default ASP.NET services to DI container. You could call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the ":ref:`di-addocelotusingbuilder-method`" section of :doc:`../features/dependencyinjection` feature. diff --git a/docs/features/servicefabric.rst b/docs/features/servicefabric.rst index 6e3906bd0c..9e4206afb7 100644 --- a/docs/features/servicefabric.rst +++ b/docs/features/servicefabric.rst @@ -9,7 +9,7 @@ We also need to set up the **ServiceDiscoveryProvider** in **GlobalConfiguration The example here shows a typical configuration. It assumes *Service Fabric* is running on ``localhost`` and that the naming service is on port ``19081``. -The example below is taken from the `samples/OcelotServiceFabric `_ folder so please check it if this doesn't make sense! +The example below is taken from the `OcelotServiceFabric `_ sample, so please check it if this doesn't make sense! .. code-block:: json @@ -24,6 +24,7 @@ The example below is taken from the `samples/OcelotServiceFabric `_ +.. _Polly: https://github.com/App-vNext/Polly +.. _@ebjornset: https://github.com/ebjornset +.. _@RaynaldM: https://github.com/RaynaldM +.. _@ArwynFr: https://github.com/ArwynFr +.. _@AlyHKafoury: https://github.com/AlyHKafoury +.. _@FelixBoers: https://github.com/FelixBoers +.. _23.2: https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0 + +Welcome to Ocelot `23.2`_ ====================================================================================== Thanks for taking a look at the Ocelot documentation! Please use the left hand navigation to get around. @@ -9,6 +17,72 @@ The main features are :doc:`../features/configuration` and :doc:`../features/rou We **do** follow development process which is described in :doc:`../building/releaseprocess`. +Release Notes +------------- + + | **Release Tag**: `23.2.0 `_ + | **Release Codename**: `Lunar Eclipse `_ + +What's new? +^^^^^^^^^^^ + +- :doc:`../features/configuration`: A brand new :ref:`config-merging-tomemory` by `@ebjornset`_ as a part of the :ref:`config-merging-files` feature. + + The ``AddOcelot`` method merges the **ocelot.*.json** files into a single **ocelot.json** file as the primary configuration file, which is written back to disk and then added to the ``IConfigurationBuilder`` for the well-known ``IConfiguration``. You can now call another ``AddOcelot`` method that adds the merged JSON directly from memory to the ``IConfigurationBuilder``, using ``AddJsonStream`` instead. + + See more details in :ref:`di-configuration-overview` of :doc:`../features/dependencyinjection`. + +- :doc:`../features/servicefabric`: Published old undocumented :ref:`sf-placeholders` feature of :doc:`../features/servicefabric` `service discovery provider `_. + + This feature by `@FelixBoers`_ is available starting from version `13.0.0 `_. + +- :doc:`../features/qualityofservice`: A brand new `Polly`_ v8 pipelines :ref:`qos-extensibility` feature by `@RaynaldM`_ + +Focus On +^^^^^^^^ + +Updates of the features +""""""""""""""""""""""" + + - :doc:`../features/configuration`: New :ref:`config-merging-tomemory` feature by `@ebjornset`_ + - :doc:`../features/dependencyinjection`: Added new overloaded :ref:`di-configuration-addocelot` by `@ebjornset`_ + - :doc:`../features/qualityofservice`: Support of new `Polly`_ v8 syntax and new :ref:`qos-extensibility` feature by `@RaynaldM`_ + +Ocelot extra packages +""""""""""""""""""""" + + - `Ocelot.Provider.Polly `_: Support of new `Polly`_ v8 syntax. + + | *Polly* `8.0+ `_ versions introduced the concept of `resilience pipelines `_. + | All `AddPolly extensions `_ have been automatically migrated from **v7** to **v8**. + | Please note that older **v7** extensions are marked with the ``[Obsolete]`` attribute and renamed using the ``V7`` suffix. And the old **v7** implementation has been moved to the `v7 namespace `_. + | See more details in :ref:`qos-polly-v7-vs-v8` section of :doc:`../features/qualityofservice` chapter. + +Stabilization aka bug fixing +"""""""""""""""""""""""""""" + + - `683 `_ by PR `1927 `_. Thanks to `@AlyHKafoury`_! + + | `New rules `_ have been added to Ocelot's configuration validation logic to find duplicate placeholders in path templates. + | See more in the `FileConfigurationFluentValidator `_ class. + + - `1518 `_ hotfix by PR `1986 `_. Thanks to `@ArwynFr`_! + + | Using the default ``IServiceCollection`` `DI extensions `_ to register Ocelot services resulted in the ``ServiceCollection`` provider being forced to be created by calling ``BuildServiceProvider()``. + | This resulted in problems with dependency injection libraries, or worse, causing the Ocelot app to crash! + | See more in the `ServiceCollectionExtensions `_ class. + + - See `all bugs `_ of the `February'24 `_ milestone + +Updated Documentation +""""""""""""""""""""" + + - :doc:`../features/configuration` + - :doc:`../features/dependencyinjection` + - :doc:`../features/qualityofservice` + - :doc:`../features/servicefabric` + + .. toctree:: :maxdepth: 2 :hidden: diff --git a/docs/introduction/gettingstarted.rst b/docs/introduction/gettingstarted.rst index 3332619926..2640b4e4be 100644 --- a/docs/introduction/gettingstarted.rst +++ b/docs/introduction/gettingstarted.rst @@ -71,7 +71,11 @@ Program ^^^^^^^ Then in your **Program.cs** you will want to have the following. -The main things to note are ``AddOcelot()`` [#f1]_ (adds Ocelot default services), ``UseOcelot().Wait()`` (sets up all the Ocelot middleware). + +The main things to note are + +* ``AddOcelot()`` adds Ocelot required and default services [#f1]_ +* ``UseOcelot().Wait()`` sets up all the Ocelot middlewares. .. code-block:: csharp @@ -120,4 +124,4 @@ The main things to note are ``AddOcelot()`` [#f1]_ (adds Ocelot default services """" -.. [#f1] The ``AddOcelot`` method adds default ASP.NET services to DI-container. You could call another more extended ``AddOcelotUsingBuilder`` method while configuring services to build and use custom builder via an ``IMvcCoreBuilder`` interface object. See more instructions in :doc:`../features/dependencyinjection`, "**The AddOcelotUsingBuilder method**" section. +.. [#f1] :ref:`di-the-addocelot-method` adds default ASP.NET services to DI container. You could call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the ":ref:`di-addocelotusingbuilder-method`" section of :doc:`../features/dependencyinjection` feature. diff --git a/samples/AdministrationApi/Issue645.postman_collection.json b/samples/Administration/Issue645.postman_collection.json similarity index 97% rename from samples/AdministrationApi/Issue645.postman_collection.json rename to samples/Administration/Issue645.postman_collection.json index c9bac89f56..6fedd16b9c 100644 --- a/samples/AdministrationApi/Issue645.postman_collection.json +++ b/samples/Administration/Issue645.postman_collection.json @@ -1,150 +1,150 @@ -{ - "info": { - "_postman_id": "6234b40a-e363-4c73-8577-1c9074abb951", - "name": "Issue645", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, - "item": [ - { - "name": "1. GET http://localhost: 55580/administration/.well-known/openid-configuration", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{AccessToken}}" - } - ], - "body": {}, - "url": { - "raw": "http://localhost:5000/administration/.well-known/openid-configuration", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "5000", - "path": [ - "administration", - ".well-known", - "openid-configuration" - ] - } - }, - "response": [] - }, - { - "name": "3. GET http://localhost: 55580/administration/configuration", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{AccessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"routes\": [\r\n {\r\n \"downstreamPathTemplate\": \"/{everything}\",\r\n \"upstreamPathTemplate\": \"/templates/{everything}\",\r\n \"upstreamHttpMethod\": [\r\n \"GET\"\r\n ],\r\n \"addHeadersToRequest\": {},\r\n \"upstreamHeaderTransform\": {},\r\n \"downstreamHeaderTransform\": {},\r\n \"addClaimsToRequest\": {},\r\n \"routeClaimsRequirement\": {},\r\n \"addQueriesToRequest\": {},\r\n \"requestIdKey\": null,\r\n \"fileCacheOptions\": {\r\n \"ttlSeconds\": 0,\r\n \"region\": null\r\n },\r\n \"routeIsCaseSensitive\": false,\r\n \"downstreamScheme\": \"http\",\r\n \"qoSOptions\": {\r\n \"exceptionsAllowedBeforeBreaking\": 0,\r\n \"durationOfBreak\": 0,\r\n \"timeoutValue\": 0\r\n },\r\n \"loadBalancerOptions\": {\r\n \"type\": null,\r\n \"key\": null,\r\n \"expiry\": 0\r\n },\r\n \"rateLimitOptions\": {\r\n \"clientWhitelist\": [],\r\n \"enableRateLimiting\": false,\r\n \"period\": null,\r\n \"periodTimespan\": 0,\r\n \"limit\": 0\r\n },\r\n \"authenticationOptions\": {\r\n \"authenticationProviderKey\": null,\r\n \"allowedScopes\": []\r\n },\r\n \"httpHandlerOptions\": {\r\n \"allowAutoRedirect\": false,\r\n \"useCookieContainer\": false,\r\n \"useTracing\": false,\r\n \"useProxy\": true\r\n },\r\n \"downstreamHostAndPorts\": [\r\n {\r\n \"host\": \"localhost\",\r\n \"port\": 50689\r\n }\r\n ],\r\n \"upstreamHost\": null,\r\n \"key\": null,\r\n \"delegatingHandlers\": [],\r\n \"priority\": 1,\r\n \"timeout\": 0,\r\n \"dangerousAcceptAnyServerCertificateValidator\": false\r\n }\r\n ],\r\n \"aggregates\": [],\r\n \"globalConfiguration\": {\r\n \"requestIdKey\": \"Request-Id\",\r\n \"rateLimitOptions\": {\r\n \"clientIdHeader\": \"ClientId\",\r\n \"quotaExceededMessage\": null,\r\n \"rateLimitCounterPrefix\": \"ocelot\",\r\n \"disableRateLimitHeaders\": false,\r\n \"httpStatusCode\": 429\r\n },\r\n \"qoSOptions\": {\r\n \"exceptionsAllowedBeforeBreaking\": 0,\r\n \"durationOfBreak\": 0,\r\n \"timeoutValue\": 0\r\n },\r\n \"baseUrl\": \"http://localhost:55580\",\r\n \"loadBalancerOptions\": {\r\n \"type\": null,\r\n \"key\": null,\r\n \"expiry\": 0\r\n },\r\n \"downstreamScheme\": null,\r\n \"httpHandlerOptions\": {\r\n \"allowAutoRedirect\": false,\r\n \"useCookieContainer\": false,\r\n \"useTracing\": false,\r\n \"useProxy\": true\r\n }\r\n }\r\n}" - }, - "url": { - "raw": "http://localhost:5000/administration/configuration", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "5000", - "path": [ - "administration", - "configuration" - ] - } - }, - "response": [] - }, - { - "name": "2. POST http://localhost: 55580/administration/connect/token", - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var jsonData = JSON.parse(responseBody);", - "postman.setGlobalVariable(\"AccessToken\", jsonData.access_token);", - "postman.setGlobalVariable(\"RefreshToken\", jsonData.refresh_token);" - ] - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "client_id", - "value": "admin", - "type": "text" - }, - { - "key": "client_secret", - "value": "secret", - "type": "text" - }, - { - "key": "scope", - "value": "admin", - "type": "text" - }, - { - "key": "grant_type", - "value": "client_credentials", - "type": "text" - } - ] - }, - "url": { - "raw": "http://localhost:5000/administration/connect/token", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "5000", - "path": [ - "administration", - "connect", - "token" - ] - } - }, - "response": [] - } - ], - "event": [ - { - "listen": "prerequest", - "script": { - "id": "0f60e7b3-e4f1-4458-bbc4-fc4809e86b2d", - "type": "text/javascript", - "exec": [ - string.Empty - ] - } - }, - { - "listen": "test", - "script": { - "id": "1279a2cf-b771-4a86-9dfa-302b240fac62", - "type": "text/javascript", - "exec": [ - string.Empty - ] - } - } - ] +{ + "info": { + "_postman_id": "6234b40a-e363-4c73-8577-1c9074abb951", + "name": "Issue645", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "1. GET http://localhost: 55580/administration/.well-known/openid-configuration", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{AccessToken}}" + } + ], + "body": {}, + "url": { + "raw": "http://localhost:5000/administration/.well-known/openid-configuration", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "administration", + ".well-known", + "openid-configuration" + ] + } + }, + "response": [] + }, + { + "name": "3. GET http://localhost: 55580/administration/configuration", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{AccessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"routes\": [\r\n {\r\n \"downstreamPathTemplate\": \"/{everything}\",\r\n \"upstreamPathTemplate\": \"/templates/{everything}\",\r\n \"upstreamHttpMethod\": [\r\n \"GET\"\r\n ],\r\n \"addHeadersToRequest\": {},\r\n \"upstreamHeaderTransform\": {},\r\n \"downstreamHeaderTransform\": {},\r\n \"addClaimsToRequest\": {},\r\n \"routeClaimsRequirement\": {},\r\n \"addQueriesToRequest\": {},\r\n \"requestIdKey\": null,\r\n \"fileCacheOptions\": {\r\n \"ttlSeconds\": 0,\r\n \"region\": null\r\n },\r\n \"routeIsCaseSensitive\": false,\r\n \"downstreamScheme\": \"http\",\r\n \"qoSOptions\": {\r\n \"exceptionsAllowedBeforeBreaking\": 0,\r\n \"durationOfBreak\": 0,\r\n \"timeoutValue\": 0\r\n },\r\n \"loadBalancerOptions\": {\r\n \"type\": null,\r\n \"key\": null,\r\n \"expiry\": 0\r\n },\r\n \"rateLimitOptions\": {\r\n \"clientWhitelist\": [],\r\n \"enableRateLimiting\": false,\r\n \"period\": null,\r\n \"periodTimespan\": 0,\r\n \"limit\": 0\r\n },\r\n \"authenticationOptions\": {\r\n \"authenticationProviderKey\": null,\r\n \"allowedScopes\": []\r\n },\r\n \"httpHandlerOptions\": {\r\n \"allowAutoRedirect\": false,\r\n \"useCookieContainer\": false,\r\n \"useTracing\": false,\r\n \"useProxy\": true\r\n },\r\n \"downstreamHostAndPorts\": [\r\n {\r\n \"host\": \"localhost\",\r\n \"port\": 50689\r\n }\r\n ],\r\n \"upstreamHost\": null,\r\n \"key\": null,\r\n \"delegatingHandlers\": [],\r\n \"priority\": 1,\r\n \"timeout\": 0,\r\n \"dangerousAcceptAnyServerCertificateValidator\": false\r\n }\r\n ],\r\n \"aggregates\": [],\r\n \"globalConfiguration\": {\r\n \"requestIdKey\": \"Request-Id\",\r\n \"rateLimitOptions\": {\r\n \"clientIdHeader\": \"ClientId\",\r\n \"quotaExceededMessage\": null,\r\n \"rateLimitCounterPrefix\": \"ocelot\",\r\n \"disableRateLimitHeaders\": false,\r\n \"httpStatusCode\": 429\r\n },\r\n \"qoSOptions\": {\r\n \"exceptionsAllowedBeforeBreaking\": 0,\r\n \"durationOfBreak\": 0,\r\n \"timeoutValue\": 0\r\n },\r\n \"baseUrl\": \"http://localhost:55580\",\r\n \"loadBalancerOptions\": {\r\n \"type\": null,\r\n \"key\": null,\r\n \"expiry\": 0\r\n },\r\n \"downstreamScheme\": null,\r\n \"httpHandlerOptions\": {\r\n \"allowAutoRedirect\": false,\r\n \"useCookieContainer\": false,\r\n \"useTracing\": false,\r\n \"useProxy\": true\r\n }\r\n }\r\n}" + }, + "url": { + "raw": "http://localhost:5000/administration/configuration", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "administration", + "configuration" + ] + } + }, + "response": [] + }, + { + "name": "2. POST http://localhost: 55580/administration/connect/token", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var jsonData = JSON.parse(responseBody);", + "postman.setGlobalVariable(\"AccessToken\", jsonData.access_token);", + "postman.setGlobalVariable(\"RefreshToken\", jsonData.refresh_token);" + ] + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "client_id", + "value": "admin", + "type": "text" + }, + { + "key": "client_secret", + "value": "secret", + "type": "text" + }, + { + "key": "scope", + "value": "admin", + "type": "text" + }, + { + "key": "grant_type", + "value": "client_credentials", + "type": "text" + } + ] + }, + "url": { + "raw": "http://localhost:5000/administration/connect/token", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "administration", + "connect", + "token" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "id": "0f60e7b3-e4f1-4458-bbc4-fc4809e86b2d", + "type": "text/javascript", + "exec": [ + string.Empty + ] + } + }, + { + "listen": "test", + "script": { + "id": "1279a2cf-b771-4a86-9dfa-302b240fac62", + "type": "text/javascript", + "exec": [ + string.Empty + ] + } + } + ] } \ No newline at end of file diff --git a/samples/AdministrationApi/AdministrationApi.csproj b/samples/Administration/Ocelot.Samples.AdministrationApi.csproj similarity index 100% rename from samples/AdministrationApi/AdministrationApi.csproj rename to samples/Administration/Ocelot.Samples.AdministrationApi.csproj diff --git a/samples/AdministrationApi/Program.cs b/samples/Administration/Program.cs similarity index 100% rename from samples/AdministrationApi/Program.cs rename to samples/Administration/Program.cs diff --git a/samples/AdministrationApi/Properties/launchSettings.json b/samples/Administration/Properties/launchSettings.json similarity index 100% rename from samples/AdministrationApi/Properties/launchSettings.json rename to samples/Administration/Properties/launchSettings.json diff --git a/samples/AdministrationApi/README.md b/samples/Administration/README.md similarity index 96% rename from samples/AdministrationApi/README.md rename to samples/Administration/README.md index 42a01b2f4e..59d236aa75 100644 --- a/samples/AdministrationApi/README.md +++ b/samples/Administration/README.md @@ -1,94 +1,94 @@ -```json -{ - "routes": [ - { - "downstreamPathTemplate": "/{everything}", - "upstreamPathTemplate": "/templates/{everything}", - "upstreamHttpMethod": [ - "GET" - ], - "addHeadersToRequest": {}, - "upstreamHeaderTransform": {}, - "downstreamHeaderTransform": {}, - "addClaimsToRequest": {}, - "routeClaimsRequirement": {}, - "addQueriesToRequest": {}, - "requestIdKey": null, - "fileCacheOptions": { - "ttlSeconds": 0, - "region": null - }, - "routeIsCaseSensitive": false, - "downstreamScheme": "http", - "qoSOptions": { - "exceptionsAllowedBeforeBreaking": 0, - "durationOfBreak": 0, - "timeoutValue": 0 - }, - "loadBalancerOptions": { - "type": null, - "key": null, - "expiry": 0 - }, - "rateLimitOptions": { - "clientWhitelist": [], - "enableRateLimiting": false, - "period": null, - "periodTimespan": 0, - "limit": 0 - }, - "authenticationOptions": { - "authenticationProviderKey": null, - "allowedScopes": [] - }, - "httpHandlerOptions": { - "allowAutoRedirect": false, - "useCookieContainer": false, - "useTracing": false, - "useProxy": true - }, - "downstreamHostAndPorts": [ - { - "host": "localhost", - "port": 50689 - } - ], - "upstreamHost": null, - "key": null, - "delegatingHandlers": [], - "priority": 1, - "timeout": 0, - "dangerousAcceptAnyServerCertificateValidator": false - } - ], - "aggregates": [], - "globalConfiguration": { - "requestIdKey": "Request-Id", - "rateLimitOptions": { - "clientIdHeader": "ClientId", - "quotaExceededMessage": null, - "rateLimitCounterPrefix": "ocelot", - "disableRateLimitHeaders": false, - "httpStatusCode": 429 - }, - "qoSOptions": { - "exceptionsAllowedBeforeBreaking": 0, - "durationOfBreak": 0, - "timeoutValue": 0 - }, - "baseUrl": "http://localhost:55580", - "loadBalancerOptions": { - "type": null, - "key": null, - "expiry": 0 - }, - "downstreamScheme": null, - "httpHandlerOptions": { - "allowAutoRedirect": false, - "useCookieContainer": false, - "useTracing": false, - "useProxy": true - } - } -} -``` +```json +{ + "routes": [ + { + "downstreamPathTemplate": "/{everything}", + "upstreamPathTemplate": "/templates/{everything}", + "upstreamHttpMethod": [ + "GET" + ], + "addHeadersToRequest": {}, + "upstreamHeaderTransform": {}, + "downstreamHeaderTransform": {}, + "addClaimsToRequest": {}, + "routeClaimsRequirement": {}, + "addQueriesToRequest": {}, + "requestIdKey": null, + "fileCacheOptions": { + "ttlSeconds": 0, + "region": null + }, + "routeIsCaseSensitive": false, + "downstreamScheme": "http", + "qoSOptions": { + "exceptionsAllowedBeforeBreaking": 0, + "durationOfBreak": 0, + "timeoutValue": 0 + }, + "loadBalancerOptions": { + "type": null, + "key": null, + "expiry": 0 + }, + "rateLimitOptions": { + "clientWhitelist": [], + "enableRateLimiting": false, + "period": null, + "periodTimespan": 0, + "limit": 0 + }, + "authenticationOptions": { + "authenticationProviderKey": null, + "allowedScopes": [] + }, + "httpHandlerOptions": { + "allowAutoRedirect": false, + "useCookieContainer": false, + "useTracing": false, + "useProxy": true + }, + "downstreamHostAndPorts": [ + { + "host": "localhost", + "port": 50689 + } + ], + "upstreamHost": null, + "key": null, + "delegatingHandlers": [], + "priority": 1, + "timeout": 0, + "dangerousAcceptAnyServerCertificateValidator": false + } + ], + "aggregates": [], + "globalConfiguration": { + "requestIdKey": "Request-Id", + "rateLimitOptions": { + "clientIdHeader": "ClientId", + "quotaExceededMessage": null, + "rateLimitCounterPrefix": "ocelot", + "disableRateLimitHeaders": false, + "httpStatusCode": 429 + }, + "qoSOptions": { + "exceptionsAllowedBeforeBreaking": 0, + "durationOfBreak": 0, + "timeoutValue": 0 + }, + "baseUrl": "http://localhost:55580", + "loadBalancerOptions": { + "type": null, + "key": null, + "expiry": 0 + }, + "downstreamScheme": null, + "httpHandlerOptions": { + "allowAutoRedirect": false, + "useCookieContainer": false, + "useTracing": false, + "useProxy": true + } + } +} +``` diff --git a/samples/AdministrationApi/appsettings.json b/samples/Administration/appsettings.json similarity index 100% rename from samples/AdministrationApi/appsettings.json rename to samples/Administration/appsettings.json diff --git a/samples/AdministrationApi/ocelot.json b/samples/Administration/ocelot.json similarity index 95% rename from samples/AdministrationApi/ocelot.json rename to samples/Administration/ocelot.json index 0fa4143fff..02e7c55129 100644 --- a/samples/AdministrationApi/ocelot.json +++ b/samples/Administration/ocelot.json @@ -1,18 +1,18 @@ -{ - "Routes": [ - { - "DownstreamPathTemplate": "/service/stats/collected", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 5100 - } - ], - "UpstreamPathTemplate": "/api/stats/collected" - } - ], - "GlobalConfiguration": { - "BaseUrl": "http://localhost:5000" - } +{ + "Routes": [ + { + "DownstreamPathTemplate": "/service/stats/collected", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 5100 + } + ], + "UpstreamPathTemplate": "/api/stats/collected" + } + ], + "GlobalConfiguration": { + "BaseUrl": "http://localhost:5000" + } } \ No newline at end of file diff --git a/samples/AdministrationApi/tempkey.rsa b/samples/Administration/tempkey.rsa similarity index 100% rename from samples/AdministrationApi/tempkey.rsa rename to samples/Administration/tempkey.rsa diff --git a/samples/OcelotBasic/Ocelot.Samples.OcelotBasic.ApiGateway.csproj b/samples/Basic/Ocelot.Samples.Basic.ApiGateway.csproj similarity index 100% rename from samples/OcelotBasic/Ocelot.Samples.OcelotBasic.ApiGateway.csproj rename to samples/Basic/Ocelot.Samples.Basic.ApiGateway.csproj diff --git a/samples/OcelotBasic/Program.cs b/samples/Basic/Program.cs similarity index 97% rename from samples/OcelotBasic/Program.cs rename to samples/Basic/Program.cs index 34c555f199..3901407c20 100644 --- a/samples/OcelotBasic/Program.cs +++ b/samples/Basic/Program.cs @@ -1,29 +1,29 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using System.IO; +using System.IO; -namespace Ocelot.Samples.OcelotBasic.ApiGateway; - -public class Program -{ - public static void Main(string[] args) - { - new WebHostBuilder() - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureAppConfiguration((hostingContext, config) => - { - config - .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) - .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) - .AddJsonFile("ocelot.json") - .AddEnvironmentVariables(); - }) - .ConfigureLogging((hostingContext, logging) => +namespace Ocelot.Samples.OcelotBasic.ApiGateway; + +public class Program +{ + public static void Main(string[] args) + { + new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) + .AddJsonFile("ocelot.json") + .AddEnvironmentVariables(); + }) + .ConfigureLogging((hostingContext, logging) => { if (hostingContext.HostingEnvironment.IsDevelopment()) { @@ -31,10 +31,10 @@ public static void Main(string[] args) logging.AddConsole(); } //add your logging - }) - .UseIISIntegration() + }) + .UseIISIntegration() .UseStartup() - .Build() - .Run(); - } -} + .Build() + .Run(); + } +} diff --git a/samples/OcelotBasic/Properties/launchSettings.json b/samples/Basic/Properties/launchSettings.json similarity index 100% rename from samples/OcelotBasic/Properties/launchSettings.json rename to samples/Basic/Properties/launchSettings.json diff --git a/samples/OcelotBasic/Startup.cs b/samples/Basic/Startup.cs similarity index 100% rename from samples/OcelotBasic/Startup.cs rename to samples/Basic/Startup.cs diff --git a/samples/OcelotBasic/appsettings.Development.json b/samples/Basic/appsettings.Development.json similarity index 100% rename from samples/OcelotBasic/appsettings.Development.json rename to samples/Basic/appsettings.Development.json diff --git a/samples/OcelotKube/ApiGateway/appsettings.json b/samples/Basic/appsettings.json similarity index 100% rename from samples/OcelotKube/ApiGateway/appsettings.json rename to samples/Basic/appsettings.json diff --git a/samples/OcelotBasic/ocelot.json b/samples/Basic/ocelot.json similarity index 95% rename from samples/OcelotBasic/ocelot.json rename to samples/Basic/ocelot.json index 2864550cd7..7cab024305 100644 --- a/samples/OcelotBasic/ocelot.json +++ b/samples/Basic/ocelot.json @@ -1,21 +1,21 @@ -{ - "Routes": [ - { - "DownstreamPathTemplate": "/todos/{id}", - "DownstreamScheme": "https", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 443 - } - ], - "UpstreamPathTemplate": "/posts/{id}", - "UpstreamHttpMethod": [ - "Get" - ] - } - ], - "GlobalConfiguration": { - "BaseUrl": "https://localhost:5000" - } +{ + "Routes": [ + { + "DownstreamPathTemplate": "/todos/{id}", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 443 + } + ], + "UpstreamPathTemplate": "/posts/{id}", + "UpstreamHttpMethod": [ + "Get" + ] + } + ], + "GlobalConfiguration": { + "BaseUrl": "https://localhost:5000" + } } diff --git a/samples/Docker/README.md b/samples/Docker/README.md deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/samples/OcelotEureka/ApiGateway/ApiGateway.csproj b/samples/Eureka/ApiGateway/Ocelot.Samples.Eureka.ApiGateway.csproj similarity index 100% rename from samples/OcelotEureka/ApiGateway/ApiGateway.csproj rename to samples/Eureka/ApiGateway/Ocelot.Samples.Eureka.ApiGateway.csproj diff --git a/samples/OcelotEureka/ApiGateway/Program.cs b/samples/Eureka/ApiGateway/Program.cs similarity index 100% rename from samples/OcelotEureka/ApiGateway/Program.cs rename to samples/Eureka/ApiGateway/Program.cs diff --git a/samples/OcelotEureka/ApiGateway/Properties/launchSettings.json b/samples/Eureka/ApiGateway/Properties/launchSettings.json similarity index 100% rename from samples/OcelotEureka/ApiGateway/Properties/launchSettings.json rename to samples/Eureka/ApiGateway/Properties/launchSettings.json diff --git a/samples/OcelotEureka/ApiGateway/appsettings.json b/samples/Eureka/ApiGateway/appsettings.json similarity index 100% rename from samples/OcelotEureka/ApiGateway/appsettings.json rename to samples/Eureka/ApiGateway/appsettings.json diff --git a/samples/OcelotEureka/ApiGateway/ocelot.json b/samples/Eureka/ApiGateway/ocelot.json similarity index 96% rename from samples/OcelotEureka/ApiGateway/ocelot.json rename to samples/Eureka/ApiGateway/ocelot.json index 5a69973de9..747cf23c80 100644 --- a/samples/OcelotEureka/ApiGateway/ocelot.json +++ b/samples/Eureka/ApiGateway/ocelot.json @@ -1,22 +1,22 @@ -{ - "Routes": [ - { - "DownstreamPathTemplate": "/api/Category", - "DownstreamScheme": "http", - "UpstreamPathTemplate": "/Category", - "ServiceName": "ncore-rat", - "UpstreamHttpMethod": [ "Get" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10000, - "TimeoutValue": 5000 - }, - "FileCacheOptions": { "TtlSeconds": 15 } - } - ], - "GlobalConfiguration": { - "RequestIdKey": "OcRequestId", - "AdministrationPath": "/administration", - "ServiceDiscoveryProvider": { "Type": "Eureka" } - } -} +{ + "Routes": [ + { + "DownstreamPathTemplate": "/api/Category", + "DownstreamScheme": "http", + "UpstreamPathTemplate": "/Category", + "ServiceName": "ncore-rat", + "UpstreamHttpMethod": [ "Get" ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10000, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + } + ], + "GlobalConfiguration": { + "RequestIdKey": "OcRequestId", + "AdministrationPath": "/administration", + "ServiceDiscoveryProvider": { "Type": "Eureka" } + } +} diff --git a/samples/OcelotEureka/DownstreamService/Controllers/CategoryController.cs b/samples/Eureka/DownstreamService/Controllers/CategoryController.cs similarity index 100% rename from samples/OcelotEureka/DownstreamService/Controllers/CategoryController.cs rename to samples/Eureka/DownstreamService/Controllers/CategoryController.cs diff --git a/samples/OcelotEureka/DownstreamService/DownstreamService.csproj b/samples/Eureka/DownstreamService/Ocelot.Samples.Eureka.DownstreamService.csproj similarity index 100% rename from samples/OcelotEureka/DownstreamService/DownstreamService.csproj rename to samples/Eureka/DownstreamService/Ocelot.Samples.Eureka.DownstreamService.csproj diff --git a/samples/OcelotEureka/DownstreamService/Program.cs b/samples/Eureka/DownstreamService/Program.cs similarity index 100% rename from samples/OcelotEureka/DownstreamService/Program.cs rename to samples/Eureka/DownstreamService/Program.cs diff --git a/samples/OcelotEureka/DownstreamService/Properties/launchSettings.json b/samples/Eureka/DownstreamService/Properties/launchSettings.json similarity index 100% rename from samples/OcelotEureka/DownstreamService/Properties/launchSettings.json rename to samples/Eureka/DownstreamService/Properties/launchSettings.json diff --git a/samples/OcelotEureka/DownstreamService/Startup.cs b/samples/Eureka/DownstreamService/Startup.cs similarity index 100% rename from samples/OcelotEureka/DownstreamService/Startup.cs rename to samples/Eureka/DownstreamService/Startup.cs diff --git a/samples/OcelotEureka/DownstreamService/appsettings.Development.json b/samples/Eureka/DownstreamService/appsettings.Development.json similarity index 100% rename from samples/OcelotEureka/DownstreamService/appsettings.Development.json rename to samples/Eureka/DownstreamService/appsettings.Development.json diff --git a/samples/OcelotEureka/DownstreamService/appsettings.json b/samples/Eureka/DownstreamService/appsettings.json similarity index 100% rename from samples/OcelotEureka/DownstreamService/appsettings.json rename to samples/Eureka/DownstreamService/appsettings.json diff --git a/samples/OcelotEureka/OcelotEureka.sln b/samples/Eureka/OcelotEureka.sln similarity index 100% rename from samples/OcelotEureka/OcelotEureka.sln rename to samples/Eureka/OcelotEureka.sln diff --git a/samples/OcelotEureka/README.md b/samples/Eureka/README.md similarity index 100% rename from samples/OcelotEureka/README.md rename to samples/Eureka/README.md diff --git a/samples/OcelotGraphQL/OcelotGraphQL.csproj b/samples/GraphQL/Ocelot.Samples.GraphQL.csproj similarity index 100% rename from samples/OcelotGraphQL/OcelotGraphQL.csproj rename to samples/GraphQL/Ocelot.Samples.GraphQL.csproj diff --git a/samples/OcelotGraphQL/OcelotGraphQL.sln b/samples/GraphQL/OcelotGraphQL.sln similarity index 100% rename from samples/OcelotGraphQL/OcelotGraphQL.sln rename to samples/GraphQL/OcelotGraphQL.sln diff --git a/samples/OcelotGraphQL/Program.cs b/samples/GraphQL/Program.cs similarity index 96% rename from samples/OcelotGraphQL/Program.cs rename to samples/GraphQL/Program.cs index e519875f1f..e2f19aaaf1 100644 --- a/samples/OcelotGraphQL/Program.cs +++ b/samples/GraphQL/Program.cs @@ -1,134 +1,134 @@ -using GraphQL; -using GraphQL.Types; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Ocelot.DependencyInjection; -using Ocelot.Middleware; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace OcelotGraphQL -{ - public class Hero - { - public int Id { get; set; } - public string Name { get; set; } - } - - public class Query - { - private readonly List _heroes = new() - { - new Hero { Id = 1, Name = "R2-D2" }, - new Hero { Id = 2, Name = "Batman" }, - new Hero { Id = 3, Name = "Wonder Woman" }, - new Hero { Id = 4, Name = "Tom Pallister" } - }; - - [GraphQLMetadata("hero")] - public Hero GetHero(int id) - { - return _heroes.FirstOrDefault(x => x.Id == id); - } - } - - public class GraphQlDelegatingHandler : DelegatingHandler - { - //private readonly ISchema _schema; - private readonly IDocumentExecuter _executer; - private readonly IDocumentWriter _writer; - - public GraphQlDelegatingHandler(IDocumentExecuter executer, IDocumentWriter writer) - { - _executer = executer; - _writer = writer; - } - - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - //try get query from body, could check http method :) - var query = await request.Content.ReadAsStringAsync(cancellationToken); - - //if not body try query string, dont hack like this in real world.. - if (query.Length == 0) - { - var decoded = WebUtility.UrlDecode(request.RequestUri.Query); - query = decoded.Replace("?query=", string.Empty); - } - - var result = await _executer.ExecuteAsync(_ => - { - _.Query = query; - }); - - var responseBody = await _writer.WriteToStringAsync(result); - - //maybe check for errors and headers etc in real world? - var response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(responseBody) - }; - - //ocelot will treat this like any other http request... - return response; - } - } - - public class Program - { - public static void Main() - { - var schema = Schema.For(@" - type Hero { - id: Int - name: String - } - - type Query { - hero(id: Int): Hero - } - ", _ => - { - _.Types.Include(); - }); - - new WebHostBuilder() - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureAppConfiguration((hostingContext, config) => - { - config - .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) - .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) - .AddJsonFile("ocelot.json", false, false) - .AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(schema); - s.AddOcelot() - .AddDelegatingHandler(); - }) - .ConfigureLogging((hostingContext, logging) => - { - logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); - logging.AddConsole(); - }) - .UseIISIntegration() - .Configure(app => - { - app.UseOcelot().Wait(); - }) - .Build() - .Run(); - } - } -} +using GraphQL; +using GraphQL.Types; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Ocelot.DependencyInjection; +using Ocelot.Middleware; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace OcelotGraphQL +{ + public class Hero + { + public int Id { get; set; } + public string Name { get; set; } + } + + public class Query + { + private readonly List _heroes = new() + { + new Hero { Id = 1, Name = "R2-D2" }, + new Hero { Id = 2, Name = "Batman" }, + new Hero { Id = 3, Name = "Wonder Woman" }, + new Hero { Id = 4, Name = "Tom Pallister" } + }; + + [GraphQLMetadata("hero")] + public Hero GetHero(int id) + { + return _heroes.FirstOrDefault(x => x.Id == id); + } + } + + public class GraphQlDelegatingHandler : DelegatingHandler + { + //private readonly ISchema _schema; + private readonly IDocumentExecuter _executer; + private readonly IDocumentWriter _writer; + + public GraphQlDelegatingHandler(IDocumentExecuter executer, IDocumentWriter writer) + { + _executer = executer; + _writer = writer; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + //try get query from body, could check http method :) + var query = await request.Content.ReadAsStringAsync(cancellationToken); + + //if not body try query string, dont hack like this in real world.. + if (query.Length == 0) + { + var decoded = WebUtility.UrlDecode(request.RequestUri.Query); + query = decoded.Replace("?query=", string.Empty); + } + + var result = await _executer.ExecuteAsync(_ => + { + _.Query = query; + }); + + var responseBody = await _writer.WriteToStringAsync(result); + + //maybe check for errors and headers etc in real world? + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(responseBody) + }; + + //ocelot will treat this like any other http request... + return response; + } + } + + public class Program + { + public static void Main() + { + var schema = Schema.For(@" + type Hero { + id: Int + name: String + } + + type Query { + hero(id: Int): Hero + } + ", _ => + { + _.Types.Include(); + }); + + new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) + .AddJsonFile("ocelot.json", false, false) + .AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(schema); + s.AddOcelot() + .AddDelegatingHandler(); + }) + .ConfigureLogging((hostingContext, logging) => + { + logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); + logging.AddConsole(); + }) + .UseIISIntegration() + .Configure(app => + { + app.UseOcelot().Wait(); + }) + .Build() + .Run(); + } + } +} diff --git a/samples/OcelotGraphQL/Properties/launchSettings.json b/samples/GraphQL/Properties/launchSettings.json similarity index 100% rename from samples/OcelotGraphQL/Properties/launchSettings.json rename to samples/GraphQL/Properties/launchSettings.json diff --git a/samples/OcelotGraphQL/README.md b/samples/GraphQL/README.md similarity index 96% rename from samples/OcelotGraphQL/README.md rename to samples/GraphQL/README.md index 7f16ed9850..1a470c344f 100644 --- a/samples/OcelotGraphQL/README.md +++ b/samples/GraphQL/README.md @@ -1,71 +1,71 @@ -# Ocelot using GraphQL example - -Loads of people keep asking me if Ocelot will every support GraphQL, in my mind Ocelot and GraphQL are two different things that can work together. -I would not try and implement GraphQL in Ocelot instead I would either have Ocelot in front of GraphQL to handle things like authorization / authentication or I would -bring in the awesome [graphql-dotnet](https://github.com/graphql-dotnet/graphql-dotnet) library and use it in a [DelegatingHandler](http://ocelot.readthedocs.io/en/latest/features/delegatinghandlers.html). This way you could have Ocelot and GraphQL without the extra hop to GraphQL. This same is an example of how to do that. - -## Example - -If you run this project with - -$ dotnet run - -Use postman or something to make the following requests and you can see Ocelot and GraphQL in action together... - -GET http://localhost:5000/graphql?query={ hero(id: 4) { id name } } - -RESPONSE -```json - { - "data": { - "hero": { - "id": 4, - "name": "Tom Pallister" - } - } - } -``` - -POST http://localhost:5000/graphql - -BODY -```json - { hero(id: 4) { id name } } -``` - -RESPONSE -```json - { - "data": { - "hero": { - "id": 4, - "name": "Tom Pallister" - } - } - } -``` - -## Notes - -Please note this project never goes out to another service, it just gets the data for GraphQL in memory. You would need to add the details of your GraphQL server in ocelot.json e.g. - -```json -{ - "Routes": [ - { - "DownstreamPathTemplate": "/graphql", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "yourgraphqlhost.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/graphql", - "DelegatingHandlers": [ - "GraphQlDelegatingHandler" - ] - } - ] - } +# Ocelot using GraphQL example + +Loads of people keep asking me if Ocelot will every support GraphQL, in my mind Ocelot and GraphQL are two different things that can work together. +I would not try and implement GraphQL in Ocelot instead I would either have Ocelot in front of GraphQL to handle things like authorization / authentication or I would +bring in the awesome [graphql-dotnet](https://github.com/graphql-dotnet/graphql-dotnet) library and use it in a [DelegatingHandler](http://ocelot.readthedocs.io/en/latest/features/delegatinghandlers.html). This way you could have Ocelot and GraphQL without the extra hop to GraphQL. This same is an example of how to do that. + +## Example + +If you run this project with + +$ dotnet run + +Use postman or something to make the following requests and you can see Ocelot and GraphQL in action together... + +GET http://localhost:5000/graphql?query={ hero(id: 4) { id name } } + +RESPONSE +```json + { + "data": { + "hero": { + "id": 4, + "name": "Tom Pallister" + } + } + } +``` + +POST http://localhost:5000/graphql + +BODY +```json + { hero(id: 4) { id name } } +``` + +RESPONSE +```json + { + "data": { + "hero": { + "id": 4, + "name": "Tom Pallister" + } + } + } +``` + +## Notes + +Please note this project never goes out to another service, it just gets the data for GraphQL in memory. You would need to add the details of your GraphQL server in ocelot.json e.g. + +```json +{ + "Routes": [ + { + "DownstreamPathTemplate": "/graphql", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "yourgraphqlhost.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/graphql", + "DelegatingHandlers": [ + "GraphQlDelegatingHandler" + ] + } + ] + } ``` \ No newline at end of file diff --git a/samples/OcelotGraphQL/ocelot.json b/samples/GraphQL/ocelot.json similarity index 96% rename from samples/OcelotGraphQL/ocelot.json rename to samples/GraphQL/ocelot.json index c716bf2587..3529e0ba83 100644 --- a/samples/OcelotGraphQL/ocelot.json +++ b/samples/GraphQL/ocelot.json @@ -1,19 +1,19 @@ -{ - "Routes": [ - { - "DownstreamPathTemplate": "/", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/graphql", - "DelegatingHandlers": [ - "GraphQlDelegatingHandler" - ] - } - ] - } +{ + "Routes": [ + { + "DownstreamPathTemplate": "/", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/graphql", + "DelegatingHandlers": [ + "GraphQlDelegatingHandler" + ] + } + ] + } \ No newline at end of file diff --git a/samples/OcelotKube/.dockerignore b/samples/Kubernetes/.dockerignore similarity index 100% rename from samples/OcelotKube/.dockerignore rename to samples/Kubernetes/.dockerignore diff --git a/samples/OcelotKube/ApiGateway/Dockerfile b/samples/Kubernetes/ApiGateway/Dockerfile similarity index 100% rename from samples/OcelotKube/ApiGateway/Dockerfile rename to samples/Kubernetes/ApiGateway/Dockerfile diff --git a/samples/OcelotKube/ApiGateway/Ocelot.Samples.OcelotKube.ApiGateway.csproj b/samples/Kubernetes/ApiGateway/Ocelot.Samples.Kubernetes.ApiGateway.csproj similarity index 80% rename from samples/OcelotKube/ApiGateway/Ocelot.Samples.OcelotKube.ApiGateway.csproj rename to samples/Kubernetes/ApiGateway/Ocelot.Samples.Kubernetes.ApiGateway.csproj index f88cd602b1..e62d4e98b9 100644 --- a/samples/OcelotKube/ApiGateway/Ocelot.Samples.OcelotKube.ApiGateway.csproj +++ b/samples/Kubernetes/ApiGateway/Ocelot.Samples.Kubernetes.ApiGateway.csproj @@ -6,9 +6,6 @@ InProcess Linux - - - diff --git a/samples/OcelotKube/ApiGateway/Program.cs b/samples/Kubernetes/ApiGateway/Program.cs similarity index 100% rename from samples/OcelotKube/ApiGateway/Program.cs rename to samples/Kubernetes/ApiGateway/Program.cs diff --git a/samples/OcelotKube/ApiGateway/Properties/launchSettings.json b/samples/Kubernetes/ApiGateway/Properties/launchSettings.json similarity index 100% rename from samples/OcelotKube/ApiGateway/Properties/launchSettings.json rename to samples/Kubernetes/ApiGateway/Properties/launchSettings.json diff --git a/samples/OcelotKube/ApiGateway/Startup.cs b/samples/Kubernetes/ApiGateway/Startup.cs similarity index 100% rename from samples/OcelotKube/ApiGateway/Startup.cs rename to samples/Kubernetes/ApiGateway/Startup.cs diff --git a/samples/OcelotKube/ApiGateway/appsettings.Development.json b/samples/Kubernetes/ApiGateway/appsettings.Development.json similarity index 100% rename from samples/OcelotKube/ApiGateway/appsettings.Development.json rename to samples/Kubernetes/ApiGateway/appsettings.Development.json diff --git a/samples/OcelotKube/DownstreamService/appsettings.json b/samples/Kubernetes/ApiGateway/appsettings.json similarity index 100% rename from samples/OcelotKube/DownstreamService/appsettings.json rename to samples/Kubernetes/ApiGateway/appsettings.json diff --git a/samples/OcelotKube/ApiGateway/ocelot.json b/samples/Kubernetes/ApiGateway/ocelot.json similarity index 95% rename from samples/OcelotKube/ApiGateway/ocelot.json rename to samples/Kubernetes/ApiGateway/ocelot.json index 6a28b9eec4..f4a5af0b86 100644 --- a/samples/OcelotKube/ApiGateway/ocelot.json +++ b/samples/Kubernetes/ApiGateway/ocelot.json @@ -1,20 +1,20 @@ -{ - "Routes": [ - { - "DownstreamPathTemplate": "/api/values", - "DownstreamScheme": "http", - "UpstreamPathTemplate": "/values", - "ServiceName": "downstreamservice", - "UpstreamHttpMethod": [ "Get" ] - } - ], - "GlobalConfiguration": { - "ServiceDiscoveryProvider": { - "Host": "192.168.0.13", - "Port": 443, - "Token": "txpc696iUhbVoudg164r93CxDTrKRVWG", - "Namespace": "dev", - "Type": "kube" - } - } -} +{ + "Routes": [ + { + "DownstreamPathTemplate": "/api/values", + "DownstreamScheme": "http", + "UpstreamPathTemplate": "/values", + "ServiceName": "downstreamservice", + "UpstreamHttpMethod": [ "Get" ] + } + ], + "GlobalConfiguration": { + "ServiceDiscoveryProvider": { + "Host": "192.168.0.13", + "Port": 443, + "Token": "txpc696iUhbVoudg164r93CxDTrKRVWG", + "Namespace": "dev", + "Type": "kube" + } + } +} diff --git a/samples/OcelotKube/Dockerfile b/samples/Kubernetes/Dockerfile similarity index 100% rename from samples/OcelotKube/Dockerfile rename to samples/Kubernetes/Dockerfile diff --git a/samples/OcelotKube/DownstreamService/Controllers/ValuesController.cs b/samples/Kubernetes/DownstreamService/Controllers/ValuesController.cs similarity index 100% rename from samples/OcelotKube/DownstreamService/Controllers/ValuesController.cs rename to samples/Kubernetes/DownstreamService/Controllers/ValuesController.cs diff --git a/samples/OcelotKube/DownstreamService/Controllers/WeatherForecastController.cs b/samples/Kubernetes/DownstreamService/Controllers/WeatherForecastController.cs similarity index 100% rename from samples/OcelotKube/DownstreamService/Controllers/WeatherForecastController.cs rename to samples/Kubernetes/DownstreamService/Controllers/WeatherForecastController.cs diff --git a/samples/OcelotKube/DownstreamService/Dockerfile b/samples/Kubernetes/DownstreamService/Dockerfile similarity index 100% rename from samples/OcelotKube/DownstreamService/Dockerfile rename to samples/Kubernetes/DownstreamService/Dockerfile diff --git a/samples/OcelotKube/DownstreamService/Models/WeatherForecast.cs b/samples/Kubernetes/DownstreamService/Models/WeatherForecast.cs similarity index 100% rename from samples/OcelotKube/DownstreamService/Models/WeatherForecast.cs rename to samples/Kubernetes/DownstreamService/Models/WeatherForecast.cs diff --git a/samples/OcelotKube/DownstreamService/Ocelot.Samples.OcelotKube.DownstreamService.csproj b/samples/Kubernetes/DownstreamService/Ocelot.Samples.Kubernetes.DownstreamService.csproj similarity index 86% rename from samples/OcelotKube/DownstreamService/Ocelot.Samples.OcelotKube.DownstreamService.csproj rename to samples/Kubernetes/DownstreamService/Ocelot.Samples.Kubernetes.DownstreamService.csproj index 3c1cfefb22..72b36b2fcb 100644 --- a/samples/OcelotKube/DownstreamService/Ocelot.Samples.OcelotKube.DownstreamService.csproj +++ b/samples/Kubernetes/DownstreamService/Ocelot.Samples.Kubernetes.DownstreamService.csproj @@ -1,6 +1,6 @@ - net7.0 + net6.0;net7.0;net8.0 disable disable InProcess diff --git a/samples/OcelotKube/DownstreamService/Program.cs b/samples/Kubernetes/DownstreamService/Program.cs similarity index 100% rename from samples/OcelotKube/DownstreamService/Program.cs rename to samples/Kubernetes/DownstreamService/Program.cs diff --git a/samples/OcelotKube/DownstreamService/Properties/launchSettings.json b/samples/Kubernetes/DownstreamService/Properties/launchSettings.json similarity index 100% rename from samples/OcelotKube/DownstreamService/Properties/launchSettings.json rename to samples/Kubernetes/DownstreamService/Properties/launchSettings.json diff --git a/samples/OcelotKube/DownstreamService/appsettings.Development.json b/samples/Kubernetes/DownstreamService/appsettings.Development.json similarity index 100% rename from samples/OcelotKube/DownstreamService/appsettings.Development.json rename to samples/Kubernetes/DownstreamService/appsettings.Development.json diff --git a/samples/OcelotBasic/appsettings.json b/samples/Kubernetes/DownstreamService/appsettings.json similarity index 92% rename from samples/OcelotBasic/appsettings.json rename to samples/Kubernetes/DownstreamService/appsettings.json index 7376aada1c..def9159a7d 100644 --- a/samples/OcelotBasic/appsettings.json +++ b/samples/Kubernetes/DownstreamService/appsettings.json @@ -1,8 +1,8 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Warning" - } - }, - "AllowedHosts": "*" -} +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/OcelotKube/OcelotKube.sln b/samples/Kubernetes/OcelotKube.sln similarity index 100% rename from samples/OcelotKube/OcelotKube.sln rename to samples/Kubernetes/OcelotKube.sln diff --git a/samples/Ocelot.Samples.sln b/samples/Ocelot.Samples.sln new file mode 100644 index 0000000000..ca208f4a69 --- /dev/null +++ b/samples/Ocelot.Samples.sln @@ -0,0 +1,91 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.AdministrationApi", "Administration\Ocelot.Samples.AdministrationApi.csproj", "{238467FE-19EE-4102-9AF7-51EB2C6F0354}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Basic.ApiGateway", "Basic\Ocelot.Samples.Basic.ApiGateway.csproj", "{A7D2C43A-E35C-4A89-AEE5-5C87052ECD89}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Eureka.ApiGateway", "Eureka\ApiGateway\Ocelot.Samples.Eureka.ApiGateway.csproj", "{EA0E146F-2C2B-4176-B6EC-F62A587F5077}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Eureka.DownstreamService", "Eureka\DownstreamService\Ocelot.Samples.Eureka.DownstreamService.csproj", "{B7317B64-2208-472D-90AC-F42B61956B79}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.GraphQL", "GraphQL\Ocelot.Samples.GraphQL.csproj", "{6CCA3677-420A-4294-8D41-67CF3D818575}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Kubernetes.ApiGateway", "Kubernetes\ApiGateway\Ocelot.Samples.Kubernetes.ApiGateway.csproj", "{721C1737-70CB-4B11-A19B-C7AAC6856CC7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Kubernetes.DownstreamService", "Kubernetes\DownstreamService\Ocelot.Samples.Kubernetes.DownstreamService.csproj", "{CE949A5D-9D25-46E3-B59A-DA63F7ED9A59}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.OpenTracing", "OpenTracing\Ocelot.Samples.OpenTracing.csproj", "{707BD584-3CC0-4087-820C-049C3D68F6A3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.ApiGateway", "ServiceDiscovery\ApiGateway\Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj", "{96B9F16E-C95D-425A-A419-40CB3C90CB77}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.DownstreamService", "ServiceDiscovery\DownstreamService\Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj", "{60E14B1A-C295-453B-910E-58E09F5A28AA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceFabric.ApiGateway", "ServiceFabric\ApiGateway\Ocelot.Samples.ServiceFabric.ApiGateway.csproj", "{115F7934-3326-492A-B131-64F0EAEBAD71}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceFabric.DownstreamService", "ServiceFabric\DownstreamService\Ocelot.Samples.ServiceFabric.DownstreamService.csproj", "{6C777A20-F557-45CF-B87B-11E3C6B29A36}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {238467FE-19EE-4102-9AF7-51EB2C6F0354}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {238467FE-19EE-4102-9AF7-51EB2C6F0354}.Debug|Any CPU.Build.0 = Debug|Any CPU + {238467FE-19EE-4102-9AF7-51EB2C6F0354}.Release|Any CPU.ActiveCfg = Release|Any CPU + {238467FE-19EE-4102-9AF7-51EB2C6F0354}.Release|Any CPU.Build.0 = Release|Any CPU + {A7D2C43A-E35C-4A89-AEE5-5C87052ECD89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7D2C43A-E35C-4A89-AEE5-5C87052ECD89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7D2C43A-E35C-4A89-AEE5-5C87052ECD89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7D2C43A-E35C-4A89-AEE5-5C87052ECD89}.Release|Any CPU.Build.0 = Release|Any CPU + {EA0E146F-2C2B-4176-B6EC-F62A587F5077}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA0E146F-2C2B-4176-B6EC-F62A587F5077}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA0E146F-2C2B-4176-B6EC-F62A587F5077}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA0E146F-2C2B-4176-B6EC-F62A587F5077}.Release|Any CPU.Build.0 = Release|Any CPU + {B7317B64-2208-472D-90AC-F42B61956B79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7317B64-2208-472D-90AC-F42B61956B79}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7317B64-2208-472D-90AC-F42B61956B79}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7317B64-2208-472D-90AC-F42B61956B79}.Release|Any CPU.Build.0 = Release|Any CPU + {6CCA3677-420A-4294-8D41-67CF3D818575}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6CCA3677-420A-4294-8D41-67CF3D818575}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6CCA3677-420A-4294-8D41-67CF3D818575}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6CCA3677-420A-4294-8D41-67CF3D818575}.Release|Any CPU.Build.0 = Release|Any CPU + {721C1737-70CB-4B11-A19B-C7AAC6856CC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {721C1737-70CB-4B11-A19B-C7AAC6856CC7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {721C1737-70CB-4B11-A19B-C7AAC6856CC7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {721C1737-70CB-4B11-A19B-C7AAC6856CC7}.Release|Any CPU.Build.0 = Release|Any CPU + {CE949A5D-9D25-46E3-B59A-DA63F7ED9A59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE949A5D-9D25-46E3-B59A-DA63F7ED9A59}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE949A5D-9D25-46E3-B59A-DA63F7ED9A59}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE949A5D-9D25-46E3-B59A-DA63F7ED9A59}.Release|Any CPU.Build.0 = Release|Any CPU + {707BD584-3CC0-4087-820C-049C3D68F6A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {707BD584-3CC0-4087-820C-049C3D68F6A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {707BD584-3CC0-4087-820C-049C3D68F6A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {707BD584-3CC0-4087-820C-049C3D68F6A3}.Release|Any CPU.Build.0 = Release|Any CPU + {96B9F16E-C95D-425A-A419-40CB3C90CB77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {96B9F16E-C95D-425A-A419-40CB3C90CB77}.Debug|Any CPU.Build.0 = Debug|Any CPU + {96B9F16E-C95D-425A-A419-40CB3C90CB77}.Release|Any CPU.ActiveCfg = Release|Any CPU + {96B9F16E-C95D-425A-A419-40CB3C90CB77}.Release|Any CPU.Build.0 = Release|Any CPU + {60E14B1A-C295-453B-910E-58E09F5A28AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {60E14B1A-C295-453B-910E-58E09F5A28AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {60E14B1A-C295-453B-910E-58E09F5A28AA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {60E14B1A-C295-453B-910E-58E09F5A28AA}.Release|Any CPU.Build.0 = Release|Any CPU + {115F7934-3326-492A-B131-64F0EAEBAD71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {115F7934-3326-492A-B131-64F0EAEBAD71}.Debug|Any CPU.Build.0 = Debug|Any CPU + {115F7934-3326-492A-B131-64F0EAEBAD71}.Release|Any CPU.ActiveCfg = Release|Any CPU + {115F7934-3326-492A-B131-64F0EAEBAD71}.Release|Any CPU.Build.0 = Release|Any CPU + {6C777A20-F557-45CF-B87B-11E3C6B29A36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C777A20-F557-45CF-B87B-11E3C6B29A36}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C777A20-F557-45CF-B87B-11E3C6B29A36}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C777A20-F557-45CF-B87B-11E3C6B29A36}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2C1620D4-EB38-4C3E-9FC5-029FB6B2F426} + EndGlobalSection +EndGlobal diff --git a/samples/OcelotServiceDiscovery/DownstreamService/.dockerignore b/samples/OcelotServiceDiscovery/DownstreamService/.dockerignore deleted file mode 100644 index e7b690f114..0000000000 --- a/samples/OcelotServiceDiscovery/DownstreamService/.dockerignore +++ /dev/null @@ -1,25 +0,0 @@ -**/.classpath -**/.dockerignore -**/.env -**/.git -**/.gitignore -**/.project -**/.settings -**/.toolstarget -**/.vs -**/.vscode -**/*.*proj.user -**/*.dbmdl -**/*.jfm -**/azds.yaml -**/bin -**/charts -**/docker-compose* -**/Dockerfile* -**/node_modules -**/npm-debug.log -**/obj -**/secrets.dev.yaml -**/values.dev.yaml -LICENSE -README.md diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Dockerfile b/samples/OcelotServiceDiscovery/DownstreamService/Dockerfile deleted file mode 100644 index b7535cfcdd..0000000000 --- a/samples/OcelotServiceDiscovery/DownstreamService/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. - -FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base -WORKDIR /app -EXPOSE 80 -EXPOSE 443 - -FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build -WORKDIR /src -COPY ["Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj", "."] -RUN dotnet restore "./Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj" -COPY . . -WORKDIR "/src/." -RUN dotnet build "Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj" -c Release -o /app/build - -FROM build AS publish -RUN dotnet publish "Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj" -c Release -o /app/publish /p:UseAppHost=false - -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "Ocelot.Samples.ServiceDiscovery.DownstreamService.dll"] diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj b/samples/OcelotServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj deleted file mode 100644 index b7c731bead..0000000000 --- a/samples/OcelotServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - net7.0 - disable - enable - Linux - . - d5492aa8-b50c-41ae-a044-9954846db9ac - - - - - - - - - diff --git a/samples/OcelotOpenTracing/OcelotOpenTracing.csproj b/samples/OpenTracing/Ocelot.Samples.OpenTracing.csproj similarity index 100% rename from samples/OcelotOpenTracing/OcelotOpenTracing.csproj rename to samples/OpenTracing/Ocelot.Samples.OpenTracing.csproj diff --git a/samples/OcelotOpenTracing/Program.cs b/samples/OpenTracing/Program.cs similarity index 100% rename from samples/OcelotOpenTracing/Program.cs rename to samples/OpenTracing/Program.cs diff --git a/samples/OcelotOpenTracing/appsettings.Development.json b/samples/OpenTracing/appsettings.Development.json similarity index 100% rename from samples/OcelotOpenTracing/appsettings.Development.json rename to samples/OpenTracing/appsettings.Development.json diff --git a/samples/OcelotOpenTracing/appsettings.json b/samples/OpenTracing/appsettings.json similarity index 100% rename from samples/OcelotOpenTracing/appsettings.json rename to samples/OpenTracing/appsettings.json diff --git a/samples/OcelotOpenTracing/ocelot.json b/samples/OpenTracing/ocelot.json similarity index 100% rename from samples/OcelotOpenTracing/ocelot.json rename to samples/OpenTracing/ocelot.json diff --git a/samples/OcelotServiceDiscovery/.dockerignore b/samples/ServiceDiscovery/.dockerignore similarity index 100% rename from samples/OcelotServiceDiscovery/.dockerignore rename to samples/ServiceDiscovery/.dockerignore diff --git a/samples/OcelotServiceDiscovery/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj b/samples/ServiceDiscovery/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj similarity index 59% rename from samples/OcelotServiceDiscovery/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj rename to samples/ServiceDiscovery/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj index aac1dd20ab..815c793e9f 100644 --- a/samples/OcelotServiceDiscovery/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj +++ b/samples/ServiceDiscovery/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj @@ -1,6 +1,8 @@ - net6.0;net7.0;net8.0 + net8.0 + disable + disable diff --git a/samples/OcelotServiceDiscovery/ApiGateway/Program.cs b/samples/ServiceDiscovery/ApiGateway/Program.cs similarity index 100% rename from samples/OcelotServiceDiscovery/ApiGateway/Program.cs rename to samples/ServiceDiscovery/ApiGateway/Program.cs diff --git a/samples/OcelotServiceDiscovery/ApiGateway/Properties/launchSettings.json b/samples/ServiceDiscovery/ApiGateway/Properties/launchSettings.json similarity index 100% rename from samples/OcelotServiceDiscovery/ApiGateway/Properties/launchSettings.json rename to samples/ServiceDiscovery/ApiGateway/Properties/launchSettings.json diff --git a/samples/OcelotServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProvider.cs b/samples/ServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProvider.cs similarity index 100% rename from samples/OcelotServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProvider.cs rename to samples/ServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProvider.cs diff --git a/samples/OcelotServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProviderFactory.cs b/samples/ServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProviderFactory.cs similarity index 100% rename from samples/OcelotServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProviderFactory.cs rename to samples/ServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProviderFactory.cs diff --git a/samples/OcelotServiceDiscovery/ApiGateway/appsettings.json b/samples/ServiceDiscovery/ApiGateway/appsettings.json similarity index 100% rename from samples/OcelotServiceDiscovery/ApiGateway/appsettings.json rename to samples/ServiceDiscovery/ApiGateway/appsettings.json diff --git a/samples/OcelotServiceDiscovery/ApiGateway/ocelot.json b/samples/ServiceDiscovery/ApiGateway/ocelot.json similarity index 100% rename from samples/OcelotServiceDiscovery/ApiGateway/ocelot.json rename to samples/ServiceDiscovery/ApiGateway/ocelot.json diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Controllers/CategoriesController.cs b/samples/ServiceDiscovery/DownstreamService/Controllers/CategoriesController.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Controllers/CategoriesController.cs rename to samples/ServiceDiscovery/DownstreamService/Controllers/CategoriesController.cs diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Controllers/HealthController.cs b/samples/ServiceDiscovery/DownstreamService/Controllers/HealthController.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Controllers/HealthController.cs rename to samples/ServiceDiscovery/DownstreamService/Controllers/HealthController.cs diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Controllers/WeatherForecastController.cs b/samples/ServiceDiscovery/DownstreamService/Controllers/WeatherForecastController.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Controllers/WeatherForecastController.cs rename to samples/ServiceDiscovery/DownstreamService/Controllers/WeatherForecastController.cs diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Models/HealthResult.cs b/samples/ServiceDiscovery/DownstreamService/Models/HealthResult.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Models/HealthResult.cs rename to samples/ServiceDiscovery/DownstreamService/Models/HealthResult.cs diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Models/MicroserviceResult.cs b/samples/ServiceDiscovery/DownstreamService/Models/MicroserviceResult.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Models/MicroserviceResult.cs rename to samples/ServiceDiscovery/DownstreamService/Models/MicroserviceResult.cs diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Models/ReadyResult.cs b/samples/ServiceDiscovery/DownstreamService/Models/ReadyResult.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Models/ReadyResult.cs rename to samples/ServiceDiscovery/DownstreamService/Models/ReadyResult.cs diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Models/WeatherForecast.cs b/samples/ServiceDiscovery/DownstreamService/Models/WeatherForecast.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Models/WeatherForecast.cs rename to samples/ServiceDiscovery/DownstreamService/Models/WeatherForecast.cs diff --git a/samples/ServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj b/samples/ServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj new file mode 100644 index 0000000000..163a956bb0 --- /dev/null +++ b/samples/ServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + disable + enable + + + + + + + diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Program.cs b/samples/ServiceDiscovery/DownstreamService/Program.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Program.cs rename to samples/ServiceDiscovery/DownstreamService/Program.cs diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Properties/launchSettings.json b/samples/ServiceDiscovery/DownstreamService/Properties/launchSettings.json similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Properties/launchSettings.json rename to samples/ServiceDiscovery/DownstreamService/Properties/launchSettings.json diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Startup.cs b/samples/ServiceDiscovery/DownstreamService/Startup.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Startup.cs rename to samples/ServiceDiscovery/DownstreamService/Startup.cs diff --git a/samples/OcelotServiceDiscovery/DownstreamService/appsettings.Development.json b/samples/ServiceDiscovery/DownstreamService/appsettings.Development.json similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/appsettings.Development.json rename to samples/ServiceDiscovery/DownstreamService/appsettings.Development.json diff --git a/samples/OcelotServiceDiscovery/DownstreamService/appsettings.json b/samples/ServiceDiscovery/DownstreamService/appsettings.json similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/appsettings.json rename to samples/ServiceDiscovery/DownstreamService/appsettings.json diff --git a/samples/OcelotServiceDiscovery/Ocelot.Samples.ServiceDiscovery.sln b/samples/ServiceDiscovery/Ocelot.Samples.ServiceDiscovery.sln similarity index 100% rename from samples/OcelotServiceDiscovery/Ocelot.Samples.ServiceDiscovery.sln rename to samples/ServiceDiscovery/Ocelot.Samples.ServiceDiscovery.sln diff --git a/samples/OcelotServiceDiscovery/README.md b/samples/ServiceDiscovery/README.md similarity index 100% rename from samples/OcelotServiceDiscovery/README.md rename to samples/ServiceDiscovery/README.md diff --git a/samples/OcelotServiceFabric/.gitignore b/samples/ServiceFabric/.gitignore similarity index 100% rename from samples/OcelotServiceFabric/.gitignore rename to samples/ServiceFabric/.gitignore diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.csproj b/samples/ServiceFabric/ApiGateway/Ocelot.Samples.ServiceFabric.ApiGateway.csproj similarity index 92% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.csproj rename to samples/ServiceFabric/ApiGateway/Ocelot.Samples.ServiceFabric.ApiGateway.csproj index c97238c53d..c9f886f75b 100644 --- a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.csproj +++ b/samples/ServiceFabric/ApiGateway/Ocelot.Samples.ServiceFabric.ApiGateway.csproj @@ -18,6 +18,6 @@ - + diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.cs b/samples/ServiceFabric/ApiGateway/OcelotApplicationApiGateway.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.cs rename to samples/ServiceFabric/ApiGateway/OcelotApplicationApiGateway.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/Program.cs b/samples/ServiceFabric/ApiGateway/Program.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/Program.cs rename to samples/ServiceFabric/ApiGateway/Program.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/Properties/launchSettings.json b/samples/ServiceFabric/ApiGateway/Properties/launchSettings.json similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/Properties/launchSettings.json rename to samples/ServiceFabric/ApiGateway/Properties/launchSettings.json diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ServiceEventListener.cs b/samples/ServiceFabric/ApiGateway/ServiceEventListener.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ServiceEventListener.cs rename to samples/ServiceFabric/ApiGateway/ServiceEventListener.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ServiceEventSource.cs b/samples/ServiceFabric/ApiGateway/ServiceEventSource.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ServiceEventSource.cs rename to samples/ServiceFabric/ApiGateway/ServiceEventSource.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/WebCommunicationListener.cs b/samples/ServiceFabric/ApiGateway/WebCommunicationListener.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/WebCommunicationListener.cs rename to samples/ServiceFabric/ApiGateway/WebCommunicationListener.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/appsettings.json b/samples/ServiceFabric/ApiGateway/appsettings.json similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/appsettings.json rename to samples/ServiceFabric/ApiGateway/appsettings.json diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ocelot.json b/samples/ServiceFabric/ApiGateway/ocelot.json similarity index 95% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ocelot.json rename to samples/ServiceFabric/ApiGateway/ocelot.json index b541e95c43..1b174cd62e 100644 --- a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ocelot.json +++ b/samples/ServiceFabric/ApiGateway/ocelot.json @@ -1,21 +1,21 @@ -{ - "Routes": [ - { - "DownstreamPathTemplate": "/api/values", - "UpstreamPathTemplate": "/EquipmentInterfaces", - "UpstreamHttpMethod": [ - "Get" - ], - "DownstreamScheme": "http", - "ServiceName": "OcelotServiceApplication/OcelotApplicationService" - } - ], - "GlobalConfiguration": { - "RequestIdKey": "OcRequestId", - "ServiceDiscoveryProvider": { - "Host": "localhost", - "Port": 19081, - "Type": "ServiceFabric" - } - } -} +{ + "Routes": [ + { + "DownstreamPathTemplate": "/api/values", + "UpstreamPathTemplate": "/EquipmentInterfaces", + "UpstreamHttpMethod": [ + "Get" + ], + "DownstreamScheme": "http", + "ServiceName": "OcelotServiceApplication/OcelotApplicationService" + } + ], + "GlobalConfiguration": { + "RequestIdKey": "OcRequestId", + "ServiceDiscoveryProvider": { + "Host": "localhost", + "Port": 19081, + "Type": "ServiceFabric" + } + } +} diff --git a/samples/OcelotServiceFabric/CONTRIBUTING.md b/samples/ServiceFabric/CONTRIBUTING.md similarity index 100% rename from samples/OcelotServiceFabric/CONTRIBUTING.md rename to samples/ServiceFabric/CONTRIBUTING.md diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/ApiGateway.cs b/samples/ServiceFabric/DownstreamService/ApiGateway.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationService/ApiGateway.cs rename to samples/ServiceFabric/DownstreamService/ApiGateway.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/Controllers/ValuesController.cs b/samples/ServiceFabric/DownstreamService/Controllers/ValuesController.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationService/Controllers/ValuesController.cs rename to samples/ServiceFabric/DownstreamService/Controllers/ValuesController.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/OcelotApplicationService.csproj b/samples/ServiceFabric/DownstreamService/Ocelot.Samples.ServiceFabric.DownstreamService.csproj similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationService/OcelotApplicationService.csproj rename to samples/ServiceFabric/DownstreamService/Ocelot.Samples.ServiceFabric.DownstreamService.csproj diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/Program.cs b/samples/ServiceFabric/DownstreamService/Program.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationService/Program.cs rename to samples/ServiceFabric/DownstreamService/Program.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/Properties/launchSettings.json b/samples/ServiceFabric/DownstreamService/Properties/launchSettings.json similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationService/Properties/launchSettings.json rename to samples/ServiceFabric/DownstreamService/Properties/launchSettings.json diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/ServiceEventSource.cs b/samples/ServiceFabric/DownstreamService/ServiceEventSource.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationService/ServiceEventSource.cs rename to samples/ServiceFabric/DownstreamService/ServiceEventSource.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/Startup.cs b/samples/ServiceFabric/DownstreamService/Startup.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationService/Startup.cs rename to samples/ServiceFabric/DownstreamService/Startup.cs diff --git a/samples/OcelotServiceFabric/LICENSE.md b/samples/ServiceFabric/LICENSE.md similarity index 100% rename from samples/OcelotServiceFabric/LICENSE.md rename to samples/ServiceFabric/LICENSE.md diff --git a/samples/OcelotServiceFabric/OcelotApplication/ApplicationManifest.xml b/samples/ServiceFabric/OcelotApplication/ApplicationManifest.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/ApplicationManifest.xml rename to samples/ServiceFabric/OcelotApplication/ApplicationManifest.xml diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.cmd b/samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.cmd similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.cmd rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.cmd diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.sh b/samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.sh similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.sh rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.sh diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/Settings.xml b/samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/Settings.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/Settings.xml rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/Settings.xml diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/_readme.txt b/samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/_readme.txt similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/_readme.txt rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/_readme.txt diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Data/_readme.txt b/samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Data/_readme.txt similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Data/_readme.txt rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Data/_readme.txt diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Linux.xml b/samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Linux.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Linux.xml rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Linux.xml diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Windows.xml b/samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Windows.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Windows.xml rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Windows.xml diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest.xml b/samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest.xml rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest.xml diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.cmd b/samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.cmd similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.cmd rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.cmd diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.sh b/samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.sh similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.sh rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.sh diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/Settings.xml b/samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/Settings.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/Settings.xml rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/Settings.xml diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/_readme.txt b/samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/_readme.txt similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/_readme.txt rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/_readme.txt diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Data/_readme.txt b/samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Data/_readme.txt similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Data/_readme.txt rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Data/_readme.txt diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Linux.xml b/samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Linux.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Linux.xml rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Linux.xml diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Windows.xml b/samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Windows.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Windows.xml rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Windows.xml diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest.xml b/samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest.xml rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest.xml diff --git a/samples/OcelotServiceFabric/README.md b/samples/ServiceFabric/README.md similarity index 100% rename from samples/OcelotServiceFabric/README.md rename to samples/ServiceFabric/README.md diff --git a/samples/OcelotServiceFabric/build.bat b/samples/ServiceFabric/build.bat similarity index 100% rename from samples/OcelotServiceFabric/build.bat rename to samples/ServiceFabric/build.bat diff --git a/samples/OcelotServiceFabric/build.sh b/samples/ServiceFabric/build.sh similarity index 100% rename from samples/OcelotServiceFabric/build.sh rename to samples/ServiceFabric/build.sh diff --git a/samples/OcelotServiceFabric/dotnet-include.sh b/samples/ServiceFabric/dotnet-include.sh similarity index 100% rename from samples/OcelotServiceFabric/dotnet-include.sh rename to samples/ServiceFabric/dotnet-include.sh diff --git a/samples/OcelotServiceFabric/install.ps1 b/samples/ServiceFabric/install.ps1 similarity index 100% rename from samples/OcelotServiceFabric/install.ps1 rename to samples/ServiceFabric/install.ps1 diff --git a/samples/OcelotServiceFabric/install.sh b/samples/ServiceFabric/install.sh similarity index 100% rename from samples/OcelotServiceFabric/install.sh rename to samples/ServiceFabric/install.sh diff --git a/samples/OcelotServiceFabric/uninstall.ps1 b/samples/ServiceFabric/uninstall.ps1 similarity index 100% rename from samples/OcelotServiceFabric/uninstall.ps1 rename to samples/ServiceFabric/uninstall.ps1 diff --git a/samples/OcelotServiceFabric/uninstall.sh b/samples/ServiceFabric/uninstall.sh similarity index 100% rename from samples/OcelotServiceFabric/uninstall.sh rename to samples/ServiceFabric/uninstall.sh diff --git a/src/Ocelot.Provider.Kubernetes/EndPointClientV1.cs b/src/Ocelot.Provider.Kubernetes/EndPointClientV1.cs index 22f58e5387..83418957b2 100644 --- a/src/Ocelot.Provider.Kubernetes/EndPointClientV1.cs +++ b/src/Ocelot.Provider.Kubernetes/EndPointClientV1.cs @@ -1,6 +1,7 @@ using HTTPlease; using KubeClient.Models; using KubeClient.ResourceClients; +using Ocelot.Provider.Kubernetes.Interfaces; namespace Ocelot.Provider.Kubernetes { diff --git a/src/Ocelot.Provider.Kubernetes/IEndPointClient.cs b/src/Ocelot.Provider.Kubernetes/Interfaces/IEndPointClient.cs similarity index 83% rename from src/Ocelot.Provider.Kubernetes/IEndPointClient.cs rename to src/Ocelot.Provider.Kubernetes/Interfaces/IEndPointClient.cs index 6dfca972db..10f79f8aff 100644 --- a/src/Ocelot.Provider.Kubernetes/IEndPointClient.cs +++ b/src/Ocelot.Provider.Kubernetes/Interfaces/IEndPointClient.cs @@ -1,7 +1,7 @@ using KubeClient.Models; using KubeClient.ResourceClients; -namespace Ocelot.Provider.Kubernetes; +namespace Ocelot.Provider.Kubernetes.Interfaces; public interface IEndPointClient : IKubeResourceClient { diff --git a/src/Ocelot.Provider.Kubernetes/Interfaces/IKubeServiceBuilder.cs b/src/Ocelot.Provider.Kubernetes/Interfaces/IKubeServiceBuilder.cs new file mode 100644 index 0000000000..d5c6bcc30e --- /dev/null +++ b/src/Ocelot.Provider.Kubernetes/Interfaces/IKubeServiceBuilder.cs @@ -0,0 +1,9 @@ +using KubeClient.Models; +using Ocelot.Values; + +namespace Ocelot.Provider.Kubernetes.Interfaces; + +public interface IKubeServiceBuilder +{ + IEnumerable BuildServices(KubeRegistryConfiguration configuration, EndpointsV1 endpoint); +} diff --git a/src/Ocelot.Provider.Kubernetes/Interfaces/IKubeServiceCreator.cs b/src/Ocelot.Provider.Kubernetes/Interfaces/IKubeServiceCreator.cs new file mode 100644 index 0000000000..a6ace7b2dd --- /dev/null +++ b/src/Ocelot.Provider.Kubernetes/Interfaces/IKubeServiceCreator.cs @@ -0,0 +1,10 @@ +using KubeClient.Models; +using Ocelot.Values; + +namespace Ocelot.Provider.Kubernetes.Interfaces; + +public interface IKubeServiceCreator +{ + IEnumerable Create(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset); + IEnumerable CreateInstance(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address); +} diff --git a/src/Ocelot.Provider.Kubernetes/Kube.cs b/src/Ocelot.Provider.Kubernetes/Kube.cs index 15b5cf6ccd..5350f43b9b 100644 --- a/src/Ocelot.Provider.Kubernetes/Kube.cs +++ b/src/Ocelot.Provider.Kubernetes/Kube.cs @@ -1,5 +1,6 @@ using KubeClient.Models; using Ocelot.Logging; +using Ocelot.Provider.Kubernetes.Interfaces; using Ocelot.Values; namespace Ocelot.Provider.Kubernetes; @@ -9,47 +10,44 @@ namespace Ocelot.Provider.Kubernetes; /// public class Kube : IServiceDiscoveryProvider { - private readonly KubeRegistryConfiguration _kubeRegistryConfiguration; + private readonly KubeRegistryConfiguration _configuration; private readonly IOcelotLogger _logger; private readonly IKubeApiClient _kubeApi; - - public Kube(KubeRegistryConfiguration kubeRegistryConfiguration, IOcelotLoggerFactory factory, IKubeApiClient kubeApi) + private readonly IKubeServiceBuilder _serviceBuilder; + private readonly List _services; + + public Kube( + KubeRegistryConfiguration configuration, + IOcelotLoggerFactory factory, + IKubeApiClient kubeApi, + IKubeServiceBuilder serviceBuilder) { - _kubeRegistryConfiguration = kubeRegistryConfiguration; + _configuration = configuration; _logger = factory.CreateLogger(); _kubeApi = kubeApi; + _serviceBuilder = serviceBuilder; + _services = new(); } - public async Task> GetAsync() + public virtual async Task> GetAsync() { var endpoint = await _kubeApi .ResourceClient(client => new EndPointClientV1(client)) - .GetAsync(_kubeRegistryConfiguration.KeyOfServiceInK8s, _kubeRegistryConfiguration.KubeNamespace); + .GetAsync(_configuration.KeyOfServiceInK8s, _configuration.KubeNamespace); - var services = new List(); - if (endpoint != null && endpoint.Subsets.Any()) + _services.Clear(); + if (endpoint?.Subsets.Count != 0) { - services.AddRange(BuildServices(endpoint)); + _services.AddRange(BuildServices(_configuration, endpoint)); } else { - _logger.LogWarning(() => $"namespace:{_kubeRegistryConfiguration.KubeNamespace}service:{_kubeRegistryConfiguration.KeyOfServiceInK8s} Unable to use ,it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"); + _logger.LogWarning(() => $"K8s Namespace:{_configuration.KubeNamespace}, Service:{_configuration.KeyOfServiceInK8s}; Unable to use: it is invalid. Address must contain host only e.g. localhost and port must be greater than 0!"); } - return services; + return _services; } - private static List BuildServices(EndpointsV1 endpoint) - { - var services = new List(); - - foreach (var subset in endpoint.Subsets) - { - services.AddRange(subset.Addresses.Select(address => new Service(endpoint.Metadata.Name, - new ServiceHostAndPort(address.Ip, subset.Ports.First().Port), - endpoint.Metadata.Uid, string.Empty, Enumerable.Empty()))); - } - - return services; - } + protected virtual IEnumerable BuildServices(KubeRegistryConfiguration configuration, EndpointsV1 endpoint) + => _serviceBuilder.BuildServices(configuration, endpoint); } diff --git a/src/Ocelot.Provider.Kubernetes/KubeRegistryConfiguration.cs b/src/Ocelot.Provider.Kubernetes/KubeRegistryConfiguration.cs index 2a3d7e8159..b264e1b67b 100644 --- a/src/Ocelot.Provider.Kubernetes/KubeRegistryConfiguration.cs +++ b/src/Ocelot.Provider.Kubernetes/KubeRegistryConfiguration.cs @@ -1,8 +1,8 @@ -namespace Ocelot.Provider.Kubernetes +namespace Ocelot.Provider.Kubernetes; + +public class KubeRegistryConfiguration { - public class KubeRegistryConfiguration - { - public string KubeNamespace { get; set; } - public string KeyOfServiceInK8s { get; set; } - } + public string KubeNamespace { get; set; } + public string KeyOfServiceInK8s { get; set; } + public string Scheme { get; set; } } diff --git a/src/Ocelot.Provider.Kubernetes/KubeServiceBuilder.cs b/src/Ocelot.Provider.Kubernetes/KubeServiceBuilder.cs new file mode 100644 index 0000000000..589cfe5bae --- /dev/null +++ b/src/Ocelot.Provider.Kubernetes/KubeServiceBuilder.cs @@ -0,0 +1,36 @@ +using KubeClient.Models; +using Ocelot.Logging; +using Ocelot.Provider.Kubernetes.Interfaces; +using Ocelot.Values; + +namespace Ocelot.Provider.Kubernetes; + +public class KubeServiceBuilder : IKubeServiceBuilder +{ + private readonly IOcelotLogger _logger; + private readonly IKubeServiceCreator _serviceCreator; + + public KubeServiceBuilder(IOcelotLoggerFactory factory, IKubeServiceCreator serviceCreator) + { + ArgumentNullException.ThrowIfNull(factory); + _logger = factory.CreateLogger(); + + ArgumentNullException.ThrowIfNull(serviceCreator); + _serviceCreator = serviceCreator; + } + + public virtual IEnumerable BuildServices(KubeRegistryConfiguration configuration, EndpointsV1 endpoint) + { + ArgumentNullException.ThrowIfNull(configuration); + ArgumentNullException.ThrowIfNull(endpoint); + + var services = endpoint.Subsets + .SelectMany(subset => _serviceCreator.Create(configuration, endpoint, subset)) + .ToArray(); + + _logger.LogDebug(() => $"K8s '{Check(endpoint.Kind)}:{Check(endpoint.ApiVersion)}:{Check(endpoint.Metadata?.Name)}' endpoint: Total built {services.Length} services."); + return services; + } + + private static string Check(string str) => string.IsNullOrEmpty(str) ? "?" : str; +} diff --git a/src/Ocelot.Provider.Kubernetes/KubeServiceCreator.cs b/src/Ocelot.Provider.Kubernetes/KubeServiceCreator.cs new file mode 100644 index 0000000000..3d51159c35 --- /dev/null +++ b/src/Ocelot.Provider.Kubernetes/KubeServiceCreator.cs @@ -0,0 +1,59 @@ +using KubeClient.Models; +using Ocelot.Logging; +using Ocelot.Provider.Kubernetes.Interfaces; +using Ocelot.Values; + +namespace Ocelot.Provider.Kubernetes; + +public class KubeServiceCreator : IKubeServiceCreator +{ + private readonly IOcelotLogger _logger; + + public KubeServiceCreator(IOcelotLoggerFactory factory) + { + ArgumentNullException.ThrowIfNull(factory); + _logger = factory.CreateLogger(); + } + + public virtual IEnumerable Create(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset) + => (configuration == null || endpoint == null || subset == null) + ? Array.Empty() + : subset.Addresses + .SelectMany(address => CreateInstance(configuration, endpoint, subset, address)) + .ToArray(); + + public virtual IEnumerable CreateInstance(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) + { + var instance = new Service( + GetServiceName(configuration, endpoint, subset, address), + GetServiceHostAndPort(configuration, endpoint, subset, address), + GetServiceId(configuration, endpoint, subset, address), + GetServiceVersion(configuration, endpoint, subset, address), + GetServiceTags(configuration, endpoint, subset, address) + ); + return new Service[] { instance }; + } + + protected virtual string GetServiceName(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) + => endpoint.Metadata?.Name; + + protected virtual ServiceHostAndPort GetServiceHostAndPort(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) + { + var ports = subset.Ports; + bool portNameToScheme(EndpointPortV1 p) => string.Equals(p.Name, configuration.Scheme, StringComparison.InvariantCultureIgnoreCase); + var portV1 = string.IsNullOrEmpty(configuration.Scheme) || !ports.Any(portNameToScheme) + ? ports.FirstOrDefault() + : ports.FirstOrDefault(portNameToScheme); + portV1 ??= new(); + portV1.Name ??= configuration.Scheme ?? string.Empty; + _logger.LogDebug(() => $"K8s service with key '{configuration.KeyOfServiceInK8s}' and address {address.Ip}; Detected port is {portV1.Name}:{portV1.Port}. Total {ports.Count} ports of [{string.Join(',', ports.Select(p => p.Name))}]."); + return new ServiceHostAndPort(address.Ip, portV1.Port, portV1.Name); + } + + protected virtual string GetServiceId(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) + => endpoint.Metadata?.Uid; + protected virtual string GetServiceVersion(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) + => endpoint.ApiVersion; + protected virtual IEnumerable GetServiceTags(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) + => Enumerable.Empty(); +} diff --git a/src/Ocelot.Provider.Kubernetes/KubernetesProviderFactory.cs b/src/Ocelot.Provider.Kubernetes/KubernetesProviderFactory.cs index 4507c03e69..a3a1d48c05 100644 --- a/src/Ocelot.Provider.Kubernetes/KubernetesProviderFactory.cs +++ b/src/Ocelot.Provider.Kubernetes/KubernetesProviderFactory.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; -using Ocelot.Logging; +using Ocelot.Logging; +using Ocelot.Provider.Kubernetes.Interfaces; namespace Ocelot.Provider.Kubernetes { @@ -17,14 +18,16 @@ private static IServiceDiscoveryProvider CreateProvider(IServiceProvider provide { var factory = provider.GetService(); var kubeClient = provider.GetService(); + var serviceBuilder = provider.GetService(); var configuration = new KubeRegistryConfiguration { KeyOfServiceInK8s = route.ServiceName, KubeNamespace = string.IsNullOrEmpty(route.ServiceNamespace) ? config.Namespace : route.ServiceNamespace, + Scheme = route.DownstreamScheme, }; - var defaultK8sProvider = new Kube(configuration, factory, kubeClient); + var defaultK8sProvider = new Kube(configuration, factory, kubeClient, serviceBuilder); return PollKube.Equals(config.Type, StringComparison.OrdinalIgnoreCase) ? new PollKube(config.PollingInterval, factory, defaultK8sProvider) diff --git a/src/Ocelot.Provider.Kubernetes/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Kubernetes/OcelotBuilderExtensions.cs index 0110bddbe3..fadedc356f 100644 --- a/src/Ocelot.Provider.Kubernetes/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Provider.Kubernetes/OcelotBuilderExtensions.cs @@ -1,16 +1,18 @@ using Microsoft.Extensions.DependencyInjection; using Ocelot.DependencyInjection; +using Ocelot.Provider.Kubernetes.Interfaces; -namespace Ocelot.Provider.Kubernetes +namespace Ocelot.Provider.Kubernetes; + +public static class OcelotBuilderExtensions { - public static class OcelotBuilderExtensions + public static IOcelotBuilder AddKubernetes(this IOcelotBuilder builder, bool usePodServiceAccount = true) { - public static IOcelotBuilder AddKubernetes(this IOcelotBuilder builder, bool usePodServiceAccount = true) - { - builder.Services - .AddSingleton(KubernetesProviderFactory.Get) - .AddKubeClient(usePodServiceAccount); - return builder; - } + builder.Services + .AddKubeClient(usePodServiceAccount) + .AddSingleton(KubernetesProviderFactory.Get) + .AddSingleton() + .AddSingleton(); + return builder; } } diff --git a/src/Ocelot.Provider.Polly/Interfaces/IPollyQoSResiliencePipelineProvider.cs b/src/Ocelot.Provider.Polly/Interfaces/IPollyQoSResiliencePipelineProvider.cs new file mode 100644 index 0000000000..599ca5c138 --- /dev/null +++ b/src/Ocelot.Provider.Polly/Interfaces/IPollyQoSResiliencePipelineProvider.cs @@ -0,0 +1,16 @@ +using Ocelot.Configuration; + +namespace Ocelot.Provider.Polly.Interfaces; + +/// Defines provider for Polly V8 pipelines. +/// An HTTP result type, usually it is type. +public interface IPollyQoSResiliencePipelineProvider + where TResult : IDisposable +{ + /// + /// Gets Polly v8 pipeline. + /// + /// The route to apply a pipeline for. + /// A object where T is . + ResiliencePipeline GetResiliencePipeline(DownstreamRoute route); +} diff --git a/src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj b/src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj index a75db00e8b..176e4769ff 100644 --- a/src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj +++ b/src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj @@ -34,7 +34,7 @@ all - + diff --git a/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs index a126336830..bfa26cf501 100644 --- a/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs @@ -6,39 +6,202 @@ using Ocelot.Errors.QoS; using Ocelot.Logging; using Ocelot.Provider.Polly.Interfaces; +using Ocelot.Provider.Polly.v7; using Ocelot.Requester; using Polly.CircuitBreaker; +using Polly.Registry; using Polly.Timeout; namespace Ocelot.Provider.Polly; public static class OcelotBuilderExtensions { - public static IOcelotBuilder AddPolly(this IOcelotBuilder builder, - QosDelegatingHandlerDelegate delegatingHandler, - Dictionary> errorMapping) - where T : class, IPollyQoSProvider + /// + /// Default mapping of Polly s to objects. + /// + public static readonly IDictionary> DefaultErrorMapping = new Dictionary> + { + {typeof(TaskCanceledException), CreateRequestTimedOutError}, + {typeof(TimeoutRejectedException), CreateRequestTimedOutError}, + {typeof(BrokenCircuitException), CreateRequestTimedOutError}, + {typeof(BrokenCircuitException), CreateRequestTimedOutError}, + }; + + private static Error CreateRequestTimedOutError(Exception e) => new RequestTimedOutError(e); + + /// + /// Adds Polly QoS provider to Ocelot by custom delegate and with custom error mapping. + /// + /// QoS provider to use (by default use ). + /// Ocelot builder to extend. + /// Your customized delegating handler (to manage QoS behavior by yourself). + /// Your customized error mapping. + /// The reference to the same extended object. + public static IOcelotBuilder AddPolly(this IOcelotBuilder builder, QosDelegatingHandlerDelegate delegatingHandler, IDictionary> errorMapping) + where TProvider : class, IPollyQoSResiliencePipelineProvider { builder.Services - .AddSingleton(errorMapping) - .AddSingleton, T>() + .AddSingleton>() + .AddSingleton(errorMapping) // Dictionary injection used in HttpExceptionToErrorMapper + .AddSingleton, TProvider>() .AddSingleton(delegatingHandler); - return builder; } + /// + /// Adds Polly QoS provider to Ocelot with custom error mapping, but default is used. + /// + /// QoS provider to use (by default use ). + /// Ocelot builder to extend. + /// Your customized error mapping. + /// The reference to the same extended object. + public static IOcelotBuilder AddPolly(this IOcelotBuilder builder, IDictionary> errorMapping) + where TProvider : class, IPollyQoSResiliencePipelineProvider + => AddPolly(builder, GetDelegatingHandler, errorMapping); + + /// + /// Adds Polly QoS provider to Ocelot with custom delegate, but default error mapping is used. + /// + /// QoS provider to use (by default use ). + /// Ocelot builder to extend. + /// Your customized delegating handler (to manage QoS behavior by yourself). + /// The reference to the same extended object. + public static IOcelotBuilder AddPolly(this IOcelotBuilder builder, QosDelegatingHandlerDelegate delegatingHandler) + where TProvider : class, IPollyQoSResiliencePipelineProvider + => AddPolly(builder, delegatingHandler, DefaultErrorMapping); + + /// + /// Adds Polly QoS provider to Ocelot by defaults. + /// + /// + /// Defaults: + /// + /// + /// + /// + /// + /// QoS provider to use (by default use ). + /// Ocelot builder to extend. + /// The reference to the same extended object. + public static IOcelotBuilder AddPolly(this IOcelotBuilder builder) + where TProvider : class, IPollyQoSResiliencePipelineProvider + => AddPolly(builder, GetDelegatingHandler, DefaultErrorMapping); + + /// + /// Adds Polly QoS provider to Ocelot by defaults with default QoS provider. + /// + /// + /// Defaults: + /// + /// + /// + /// + /// + /// + /// Ocelot builder to extend. + /// The reference to the same extended object. public static IOcelotBuilder AddPolly(this IOcelotBuilder builder) + => AddPolly(builder, GetDelegatingHandler, DefaultErrorMapping); + + /// + /// Creates default delegating handler based on the type. + /// + /// The downstream route to apply the handler for. + /// The context accessor of the route. + /// The factory of logger. + /// A object, but concrete type is the class. + private static DelegatingHandler GetDelegatingHandler(DownstreamRoute route, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory loggerFactory) + => new PollyResiliencePipelineDelegatingHandler(route, contextAccessor, loggerFactory); + + #region Obsolete extensions will be removed in future version + + /// + /// Adds Polly QoS provider to Ocelot by custom delegate and with custom error mapping. + /// + /// QoS provider to use (by default use ). + /// Ocelot builder to extend. + /// Your customized delegating handler (to manage QoS behavior by yourself). + /// Your customized error mapping. + /// The reference to the same extended object. + [Obsolete("Use AddPolly instead, it will be remove in future version")] + public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder, QosDelegatingHandlerDelegate delegatingHandler, IDictionary> errorMapping) + where TProvider : class, IPollyQoSProvider { - var errorMapping = new Dictionary> - { - { typeof(TaskCanceledException), e => new RequestTimedOutError(e) }, - { typeof(TimeoutRejectedException), e => new RequestTimedOutError(e) }, - { typeof(BrokenCircuitException), e => new RequestTimedOutError(e) }, - { typeof(BrokenCircuitException), e => new RequestTimedOutError(e) }, - }; - return AddPolly(builder, GetDelegatingHandler, errorMapping); + builder.Services + .AddSingleton(errorMapping) + .AddSingleton, TProvider>() + .AddSingleton(delegatingHandler); + return builder; } - private static DelegatingHandler GetDelegatingHandler(DownstreamRoute route, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory loggerFactory) + /// + /// Adds Polly QoS provider to Ocelot with custom error mapping, but default is used. + /// + /// QoS provider to use (by default use ). + /// Ocelot builder to extend. + /// Your customized error mapping. + /// The reference to the same extended object. + [Obsolete("Use AddPolly instead, it will be remove in future version")] + public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder, IDictionary> errorMapping) + where TProvider : class, IPollyQoSProvider + => AddPollyV7(builder, GetDelegatingHandlerV7, errorMapping); + + /// + /// Adds Polly QoS provider to Ocelot with custom delegate, but default error mapping is used. + /// + /// QoS provider to use (by default use ). + /// Ocelot builder to extend. + /// Your customized delegating handler (to manage QoS behavior by yourself). + /// The reference to the same extended object. + [Obsolete("Use AddPolly instead, it will be remove in future version")] + public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder, QosDelegatingHandlerDelegate delegatingHandler) + where TProvider : class, IPollyQoSProvider + => AddPollyV7(builder, delegatingHandler, DefaultErrorMapping); + + /// + /// Adds Polly QoS provider to Ocelot by defaults. + /// + /// + /// Defaults: + /// + /// + /// + /// + /// + /// QoS provider to use (by default use ). + /// Ocelot builder to extend. + /// The reference to the same extended object. + [Obsolete("Use AddPolly instead, it will be remove in future version")] + public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder) + where TProvider : class, IPollyQoSProvider + => AddPollyV7(builder, GetDelegatingHandlerV7, DefaultErrorMapping); + + /// + /// Adds Polly QoS provider to Ocelot by defaults with default QoS provider. + /// + /// + /// Defaults: + /// + /// + /// + /// + /// + /// + /// Ocelot builder to extend. + /// The reference to the same extended object. + [Obsolete("Use AddPolly instead, it will be remove in future version")] + public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder) + => AddPollyV7(builder, GetDelegatingHandlerV7, DefaultErrorMapping); + + /// + /// Creates default delegating handler based on the type. + /// + /// The downstream route to apply the handler for. + /// The context accessor of the route. + /// The factory of logger. + /// A object, but concrete type is the class. + private static DelegatingHandler GetDelegatingHandlerV7(DownstreamRoute route, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory loggerFactory) => new PollyPoliciesDelegatingHandler(route, contextAccessor, loggerFactory); + + #endregion } diff --git a/src/Ocelot.Provider.Polly/OcelotResiliencePipelineKey.cs b/src/Ocelot.Provider.Polly/OcelotResiliencePipelineKey.cs new file mode 100644 index 0000000000..528df61eeb --- /dev/null +++ b/src/Ocelot.Provider.Polly/OcelotResiliencePipelineKey.cs @@ -0,0 +1,12 @@ +using Polly.Registry; + +namespace Ocelot.Provider.Polly; + +/// +/// Object used to identify a resilience pipeline in . +/// +/// +/// Object used to identify a resilience pipeline in +/// +/// The key for the resilience pipeline. +public record OcelotResiliencePipelineKey(string Key); diff --git a/src/Ocelot.Provider.Polly/PollyQoSProviderBase.cs b/src/Ocelot.Provider.Polly/PollyQoSProviderBase.cs new file mode 100644 index 0000000000..24760c8005 --- /dev/null +++ b/src/Ocelot.Provider.Polly/PollyQoSProviderBase.cs @@ -0,0 +1,25 @@ +using Ocelot.Configuration; +using System.Net; + +namespace Ocelot.Provider.Polly; + +public abstract class PollyQoSProviderBase +{ + protected static readonly HashSet ServerErrorCodes = new() + { + HttpStatusCode.InternalServerError, + HttpStatusCode.NotImplemented, + HttpStatusCode.BadGateway, + HttpStatusCode.ServiceUnavailable, + HttpStatusCode.GatewayTimeout, + HttpStatusCode.HttpVersionNotSupported, + HttpStatusCode.VariantAlsoNegotiates, + HttpStatusCode.InsufficientStorage, + HttpStatusCode.LoopDetected, + }; + + protected static string GetRouteName(DownstreamRoute route) + => string.IsNullOrWhiteSpace(route.ServiceName) + ? route.UpstreamPathTemplate?.Template ?? route.DownstreamPathTemplate?.Value ?? string.Empty + : route.ServiceName; +} diff --git a/src/Ocelot.Provider.Polly/PollyQoSResiliencePipelineProvider.cs b/src/Ocelot.Provider.Polly/PollyQoSResiliencePipelineProvider.cs new file mode 100644 index 0000000000..be2df16b3e --- /dev/null +++ b/src/Ocelot.Provider.Polly/PollyQoSResiliencePipelineProvider.cs @@ -0,0 +1,84 @@ +using Ocelot.Configuration; +using Ocelot.Logging; +using Ocelot.Provider.Polly.Interfaces; +using Polly.CircuitBreaker; +using Polly.Registry; +using Polly.Timeout; + +namespace Ocelot.Provider.Polly; + +/// +/// Default provider for Polly V8 pipelines. +/// +public class PollyQoSResiliencePipelineProvider : PollyQoSProviderBase, IPollyQoSResiliencePipelineProvider +{ + private readonly ResiliencePipelineRegistry _resiliencePipelineRegistry; + private readonly IOcelotLogger _logger; + + public PollyQoSResiliencePipelineProvider(IOcelotLoggerFactory loggerFactory, + ResiliencePipelineRegistry resiliencePipelineRegistry) + { + _resiliencePipelineRegistry = resiliencePipelineRegistry; + _logger = loggerFactory.CreateLogger(); + } + + /// + /// Gets Polly V8 resilience pipeline (applies QoS feature) for the route. + /// + /// The downstream route to apply the pipeline for. + /// A object where T is . + public ResiliencePipeline GetResiliencePipeline(DownstreamRoute route) + { + var options = route.QosOptions; + + // Check if we need pipeline at all before calling GetOrAddPipeline + if (options is null || + (options.ExceptionsAllowedBeforeBreaking == 0 && options.TimeoutValue is int.MaxValue)) + { + return null; // shortcut > no qos + } + + var currentRouteName = GetRouteName(route); + return _resiliencePipelineRegistry.GetOrAddPipeline( + key: new OcelotResiliencePipelineKey(currentRouteName), + configure: (builder) => PollyResiliencePipelineWrapperFactory(builder, route)); + } + + private void PollyResiliencePipelineWrapperFactory(ResiliencePipelineBuilder builder, DownstreamRoute route) + { + var options = route.QosOptions; + + // Add TimeoutStrategy if TimeoutValue is not int.MaxValue and greater than 0 + if (options.TimeoutValue != int.MaxValue && options.TimeoutValue > 0) + { + builder.AddTimeout(TimeSpan.FromMilliseconds(options.TimeoutValue)); + } + + // Add CircuitBreakerStrategy only if ExceptionsAllowedBeforeBreaking is greater than 0 + if (options.ExceptionsAllowedBeforeBreaking <= 0) + { + return; // shortcut > no qos (no timeout, no ExceptionsAllowedBeforeBreaking) + } + + var info = $"Circuit Breaker for Route: {GetRouteName(route)}: "; + + var circuitBreakerStrategyOptions = new CircuitBreakerStrategyOptions + { + FailureRatio = 0.8, + SamplingDuration = TimeSpan.FromSeconds(10), + MinimumThroughput = options.ExceptionsAllowedBeforeBreaking, + BreakDuration = TimeSpan.FromMilliseconds(options.DurationOfBreak), + ShouldHandle = new PredicateBuilder() + .HandleResult(message => ServerErrorCodes.Contains(message.StatusCode)) + .Handle() + .Handle(), + OnOpened = args => + { + _logger.LogError(info + $"Breaking for {args.BreakDuration.TotalMilliseconds} ms", args.Outcome.Exception); + return ValueTask.CompletedTask; + }, + }; + + builder.AddCircuitBreaker(circuitBreakerStrategyOptions); + } +} diff --git a/src/Ocelot.Provider.Polly/PollyResiliencePipelineDelegatingHandler.cs b/src/Ocelot.Provider.Polly/PollyResiliencePipelineDelegatingHandler.cs new file mode 100644 index 0000000000..4d8b0d10c9 --- /dev/null +++ b/src/Ocelot.Provider.Polly/PollyResiliencePipelineDelegatingHandler.cs @@ -0,0 +1,57 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Configuration; +using Ocelot.Logging; +using Ocelot.Provider.Polly.Interfaces; +using Polly.CircuitBreaker; +using System.Diagnostics; + +namespace Ocelot.Provider.Polly; + +public class PollyResiliencePipelineDelegatingHandler : DelegatingHandler +{ + private readonly DownstreamRoute _route; + private readonly IHttpContextAccessor _contextAccessor; + private readonly IOcelotLogger _logger; + + public PollyResiliencePipelineDelegatingHandler( + DownstreamRoute route, + IHttpContextAccessor contextAccessor, + IOcelotLoggerFactory loggerFactory) + { + _route = route; + _contextAccessor = contextAccessor; + _logger = loggerFactory.CreateLogger(); + } + + private IPollyQoSResiliencePipelineProvider GetQoSProvider() + { + Debug.Assert(_contextAccessor.HttpContext != null, "_contextAccessor.HttpContext != null"); + + // TODO: Move IPollyQoSResiliencePipelineProvider object injection to DI container by a DI helper + return _contextAccessor.HttpContext.RequestServices.GetService>(); + } + + /// + /// Sends an HTTP request to the inner handler to send to the server as an asynchronous operation. + /// + /// Downstream request. + /// Token to cancel the task. + /// A object of a result. + /// Exception thrown when a circuit is broken. + /// Exception thrown by and classes. + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var qoSProvider = this.GetQoSProvider(); + var pipeline = qoSProvider.GetResiliencePipeline(_route); + + if (pipeline == null) + { + _logger.LogDebug(() => $"No {nameof(pipeline)} was detected by QoS provider for the route with downstream URL '{request.RequestUri}'."); + return await base.SendAsync(request, cancellationToken); // shortcut > no qos + } + + _logger.LogInformation(() => $"The {pipeline.GetType().Name} {nameof(pipeline)} has detected by QoS provider for the route with downstream URL '{request.RequestUri}'. Going to execute request..."); + return await pipeline.ExecuteAsync(async (token) => await base.SendAsync(request, token), cancellationToken); + } +} diff --git a/src/Ocelot.Provider.Polly/Interfaces/IPollyQoSProvider.cs b/src/Ocelot.Provider.Polly/v7/IPollyQoSProvider.cs similarity index 54% rename from src/Ocelot.Provider.Polly/Interfaces/IPollyQoSProvider.cs rename to src/Ocelot.Provider.Polly/v7/IPollyQoSProvider.cs index 2145975315..1537439b92 100644 --- a/src/Ocelot.Provider.Polly/Interfaces/IPollyQoSProvider.cs +++ b/src/Ocelot.Provider.Polly/v7/IPollyQoSProvider.cs @@ -1,7 +1,8 @@ using Ocelot.Configuration; -namespace Ocelot.Provider.Polly.Interfaces; +namespace Ocelot.Provider.Polly.v7; +[Obsolete("It is obsolete because now, we use IPollyQoSResiliencePipelineProvider with new v8 resilience strategies")] public interface IPollyQoSProvider where TResult : class { diff --git a/src/Ocelot.Provider.Polly/v7/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Polly/v7/OcelotBuilderExtensions.cs new file mode 100644 index 0000000000..6098bbe018 --- /dev/null +++ b/src/Ocelot.Provider.Polly/v7/OcelotBuilderExtensions.cs @@ -0,0 +1,120 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Configuration; +using Ocelot.DependencyInjection; +using Ocelot.Errors; +using Ocelot.Errors.QoS; +using Ocelot.Logging; +using Ocelot.Requester; +using Polly.CircuitBreaker; +using Polly.Timeout; + +namespace Ocelot.Provider.Polly.v7; + +public static class OcelotBuilderExtensions +{ + /// + /// Default mapping of Polly s to objects. + /// + public static readonly IDictionary> DefaultErrorMapping = new Dictionary> + { + {typeof(TaskCanceledException), CreateRequestTimedOutError}, + {typeof(TimeoutRejectedException), CreateRequestTimedOutError}, + {typeof(BrokenCircuitException), CreateRequestTimedOutError}, + {typeof(BrokenCircuitException), CreateRequestTimedOutError}, + }; + + private static Error CreateRequestTimedOutError(Exception e) => new RequestTimedOutError(e); + + #region Obsolete extensions will be removed in future version + + /// + /// Adds Polly QoS provider to Ocelot by custom delegate and with custom error mapping. + /// + /// QoS provider to use (by default use ). + /// Ocelot builder to extend. + /// Your customized delegating handler (to manage QoS behavior by yourself). + /// Your customized error mapping. + /// The reference to the same extended object. + [Obsolete("We advise you to use Ocelot.Provider.Polly.AddPolly rather than this one. Polly v7 support will be removed in a future version")] + public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder, QosDelegatingHandlerDelegate delegatingHandler, IDictionary> errorMapping) + where TProvider : class, IPollyQoSProvider + { + builder.Services + .AddSingleton(errorMapping) + .AddSingleton, TProvider>() + .AddSingleton(delegatingHandler); + return builder; + } + + /// + /// Adds Polly QoS provider to Ocelot with custom error mapping, but default is used. + /// + /// QoS provider to use (by default use ). + /// Ocelot builder to extend. + /// Your customized error mapping. + /// The reference to the same extended object. + [Obsolete("We advise you to use Ocelot.Provider.Polly.AddPolly rather than this one. Polly v7 support will be removed in a future version")] + public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder, IDictionary> errorMapping) + where TProvider : class, IPollyQoSProvider + => AddPollyV7(builder, GetDelegatingHandlerV7, errorMapping); + + /// + /// Adds Polly QoS provider to Ocelot with custom delegate, but default error mapping is used. + /// + /// QoS provider to use (by default use ). + /// Ocelot builder to extend. + /// Your customized delegating handler (to manage QoS behavior by yourself). + /// The reference to the same extended object. + [Obsolete("We advise you to use Ocelot.Provider.Polly.AddPolly rather than this one. Polly v7 support will be removed in a future version")] + public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder, QosDelegatingHandlerDelegate delegatingHandler) + where TProvider : class, IPollyQoSProvider + => AddPollyV7(builder, delegatingHandler, DefaultErrorMapping); + + /// + /// Adds Polly QoS provider to Ocelot by defaults. + /// + /// + /// Defaults: + /// + /// + /// + /// + /// + /// QoS provider to use (by default use ). + /// Ocelot builder to extend. + /// The reference to the same extended object. + [Obsolete("Use AddPolly instead, it will be remove in future version")] + public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder) + where TProvider : class, IPollyQoSProvider + => AddPollyV7(builder, GetDelegatingHandlerV7, DefaultErrorMapping); + + /// + /// Adds Polly QoS provider to Ocelot by defaults with default QoS provider. + /// + /// + /// Defaults: + /// + /// + /// + /// + /// + /// + /// Ocelot builder to extend. + /// The reference to the same extended object. + [Obsolete("We advise you to use Ocelot.Provider.Polly.AddPolly rather than this one. Polly v7 support will be removed in a future version")] + public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder) + => AddPollyV7(builder, GetDelegatingHandlerV7, DefaultErrorMapping); + + /// + /// Creates default delegating handler based on the type. + /// + /// The downstream route to apply the handler for. + /// The context accessor of the route. + /// The factory of logger. + /// A object, but concrete type is the class. + private static DelegatingHandler GetDelegatingHandlerV7(DownstreamRoute route, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory loggerFactory) + => new PollyPoliciesDelegatingHandler(route, contextAccessor, loggerFactory); + + #endregion +} diff --git a/src/Ocelot.Provider.Polly/PollyPoliciesDelegatingHandler.cs b/src/Ocelot.Provider.Polly/v7/PollyPoliciesDelegatingHandler.cs similarity index 86% rename from src/Ocelot.Provider.Polly/PollyPoliciesDelegatingHandler.cs rename to src/Ocelot.Provider.Polly/v7/PollyPoliciesDelegatingHandler.cs index 03be864951..162e123371 100644 --- a/src/Ocelot.Provider.Polly/PollyPoliciesDelegatingHandler.cs +++ b/src/Ocelot.Provider.Polly/v7/PollyPoliciesDelegatingHandler.cs @@ -1,13 +1,15 @@ +using System.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Logging; -using Ocelot.Provider.Polly.Interfaces; using Polly.CircuitBreaker; -using System.Diagnostics; -namespace Ocelot.Provider.Polly; +namespace Ocelot.Provider.Polly.v7; +/// Delegates sending to downstream. +/// Outdated V7 design! Use the class. +[Obsolete("Due to new v8 policy definition in Polly 8 (use PollyResiliencePipelineDelegatingHandler)")] public class PollyPoliciesDelegatingHandler : DelegatingHandler { private readonly DownstreamRoute _route; diff --git a/src/Ocelot.Provider.Polly/PollyPolicyWrapper.cs b/src/Ocelot.Provider.Polly/v7/PollyPolicyWrapper.cs similarity index 82% rename from src/Ocelot.Provider.Polly/PollyPolicyWrapper.cs rename to src/Ocelot.Provider.Polly/v7/PollyPolicyWrapper.cs index 0b3058cbb1..821743433c 100644 --- a/src/Ocelot.Provider.Polly/PollyPolicyWrapper.cs +++ b/src/Ocelot.Provider.Polly/v7/PollyPolicyWrapper.cs @@ -1,4 +1,4 @@ -namespace Ocelot.Provider.Polly; +namespace Ocelot.Provider.Polly.v7; public class PollyPolicyWrapper where TResult : class @@ -12,8 +12,8 @@ public PollyPolicyWrapper(params IAsyncPolicy[] policies) { var allPolicies = policies.Where(p => p != null).ToArray(); - AsyncPollyPolicy = allPolicies.Length > 1 ? - Policy.WrapAsync(allPolicies) : + AsyncPollyPolicy = allPolicies.Length > 1 ? + Policy.WrapAsync(allPolicies) : allPolicies[0]; } diff --git a/src/Ocelot.Provider.Polly/PollyQoSProvider.cs b/src/Ocelot.Provider.Polly/v7/PollyQoSProvider.cs similarity index 71% rename from src/Ocelot.Provider.Polly/PollyQoSProvider.cs rename to src/Ocelot.Provider.Polly/v7/PollyQoSProvider.cs index 7a9465a22e..51c727e802 100644 --- a/src/Ocelot.Provider.Polly/PollyQoSProvider.cs +++ b/src/Ocelot.Provider.Polly/v7/PollyQoSProvider.cs @@ -1,44 +1,29 @@ -using Ocelot.Configuration; +using Ocelot.Configuration; using Ocelot.Logging; -using Ocelot.Provider.Polly.Interfaces; using Polly.CircuitBreaker; using Polly.Timeout; -using System.Net; -namespace Ocelot.Provider.Polly; +namespace Ocelot.Provider.Polly.v7; -public class PollyQoSProvider : IPollyQoSProvider +/// Legacy QoS provider based on Polly v7. +/// Use the as a new QoS provider based on Polly v8. +[Obsolete("Due to new v8 policy definition in Polly 8 (use PollyQoSResiliencePipelineProvider)")] +public class PollyQoSProvider : PollyQoSProviderBase, IPollyQoSProvider { private readonly Dictionary> _policyWrappers = new(); + private readonly object _lockObject = new(); private readonly IOcelotLogger _logger; - //todo: this should be configurable and available as global config parameter in ocelot.json + // TODO: This should be configurable and available as global config parameter in ocelot.json public const int DefaultRequestTimeoutSeconds = 90; - private readonly HashSet _serverErrorCodes = new() - { - HttpStatusCode.InternalServerError, - HttpStatusCode.NotImplemented, - HttpStatusCode.BadGateway, - HttpStatusCode.ServiceUnavailable, - HttpStatusCode.GatewayTimeout, - HttpStatusCode.HttpVersionNotSupported, - HttpStatusCode.VariantAlsoNegotiates, - HttpStatusCode.InsufficientStorage, - HttpStatusCode.LoopDetected, - }; - public PollyQoSProvider(IOcelotLoggerFactory loggerFactory) { _logger = loggerFactory.CreateLogger(); } - private static string GetRouteName(DownstreamRoute route) - => string.IsNullOrWhiteSpace(route.ServiceName) - ? route.UpstreamPathTemplate?.Template ?? route.DownstreamPathTemplate?.Value ?? string.Empty - : route.ServiceName; - + [Obsolete("Due to new v8 policy definition in Polly 8 (use GetResiliencePipeline in PollyQoSResiliencePipelineProvider)")] public PollyPolicyWrapper GetPollyPolicyWrapper(DownstreamRoute route) { lock (_lockObject) @@ -61,7 +46,7 @@ private PollyPolicyWrapper PollyPolicyWrapperFactory(Downst var info = $"Route: {GetRouteName(route)}; Breaker logging in {nameof(PollyQoSProvider)}: "; exceptionsAllowedBeforeBreakingPolicy = Policy - .HandleResult(r => _serverErrorCodes.Contains(r.StatusCode)) + .HandleResult(r => ServerErrorCodes.Contains(r.StatusCode)) .Or() .Or() .CircuitBreakerAsync(route.QosOptions.ExceptionsAllowedBeforeBreaking, diff --git a/src/Ocelot/Cache/AspMemoryCache.cs b/src/Ocelot/Cache/DefaultMemoryCache.cs similarity index 94% rename from src/Ocelot/Cache/AspMemoryCache.cs rename to src/Ocelot/Cache/DefaultMemoryCache.cs index 2067b38139..f53b6ceafd 100644 --- a/src/Ocelot/Cache/AspMemoryCache.cs +++ b/src/Ocelot/Cache/DefaultMemoryCache.cs @@ -2,12 +2,12 @@ namespace Ocelot.Cache { - public class AspMemoryCache : IOcelotCache + public class DefaultMemoryCache : IOcelotCache { private readonly IMemoryCache _memoryCache; private readonly Dictionary> _regions; - public AspMemoryCache(IMemoryCache memoryCache) + public DefaultMemoryCache(IMemoryCache memoryCache) { _memoryCache = memoryCache; _regions = new Dictionary>(); diff --git a/src/Ocelot/Cache/IRegionCreator.cs b/src/Ocelot/Cache/IRegionCreator.cs deleted file mode 100644 index da1b042dab..0000000000 --- a/src/Ocelot/Cache/IRegionCreator.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Ocelot.Configuration.File; - -namespace Ocelot.Cache -{ - public interface IRegionCreator - { - string Create(FileRoute route); - } -} diff --git a/src/Ocelot/Cache/RegionCreator.cs b/src/Ocelot/Cache/RegionCreator.cs deleted file mode 100644 index c2dd0ccac6..0000000000 --- a/src/Ocelot/Cache/RegionCreator.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Ocelot.Configuration.File; - -namespace Ocelot.Cache -{ - public class RegionCreator : IRegionCreator - { - public string Create(FileRoute route) - { - if (!string.IsNullOrEmpty(route?.FileCacheOptions?.Region)) - { - return route?.FileCacheOptions?.Region; - } - - var methods = string.Join(string.Empty, route.UpstreamHttpMethod.Select(m => m)); - - var region = $"{methods}{route.UpstreamPathTemplate.Replace("/", string.Empty)}"; - - return region; - } - } -} diff --git a/src/Ocelot/Configuration/AuthenticationOptions.cs b/src/Ocelot/Configuration/AuthenticationOptions.cs index af5cf7273e..978a940a8a 100644 --- a/src/Ocelot/Configuration/AuthenticationOptions.cs +++ b/src/Ocelot/Configuration/AuthenticationOptions.cs @@ -8,22 +8,21 @@ public AuthenticationOptions(List allowedScopes, string authenticationPr { AllowedScopes = allowedScopes; AuthenticationProviderKey = authenticationProviderKey; - AuthenticationProviderKeys = []; + AuthenticationProviderKeys = Array.Empty(); } public AuthenticationOptions(FileAuthenticationOptions from) { - AllowedScopes = from.AllowedScopes ?? []; + AllowedScopes = from.AllowedScopes ?? new(); AuthenticationProviderKey = from.AuthenticationProviderKey ?? string.Empty; - AuthenticationProviderKeys = from.AuthenticationProviderKeys ?? []; + AuthenticationProviderKeys = from.AuthenticationProviderKeys ?? Array.Empty(); } - public AuthenticationOptions(List allowedScopes, string authenticationProviderKey, - string[] authenticationProviderKeys) + public AuthenticationOptions(List allowedScopes, string authenticationProviderKey, string[] authenticationProviderKeys) { - AllowedScopes = allowedScopes ?? []; + AllowedScopes = allowedScopes ?? new(); AuthenticationProviderKey = authenticationProviderKey ?? string.Empty; - AuthenticationProviderKeys = authenticationProviderKeys ?? []; + AuthenticationProviderKeys = authenticationProviderKeys ?? Array.Empty(); } public List AllowedScopes { get; } diff --git a/src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs b/src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs index e911908c7d..0b85ee8095 100644 --- a/src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs +++ b/src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs @@ -4,7 +4,7 @@ public class AuthenticationOptionsBuilder { private List _allowedScopes = new(); private string _authenticationProviderKey; - private string[] _authenticationProviderKeys =[]; + private string[] _authenticationProviderKeys = Array.Empty(); public AuthenticationOptionsBuilder WithAllowedScopes(List allowedScopes) { diff --git a/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs index 9dc93008ee..281e930332 100644 --- a/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs @@ -19,7 +19,7 @@ public class DownstreamRouteBuilder private List _claimToDownstreamPath; private string _requestIdHeaderKey; private bool _isCached; - private CacheOptions _fileCacheOptions; + private CacheOptions _cacheOptions; private string _downstreamScheme; private LoadBalancerOptions _loadBalancerOptions; private QoSOptions _qosOptions; @@ -87,7 +87,7 @@ public DownstreamRouteBuilder WithUpstreamPathTemplate(UpstreamPathTemplate inpu public DownstreamRouteBuilder WithUpstreamHttpMethod(List input) { - _upstreamHttpMethod = (input.Count == 0) ? new List() : input.Select(x => new HttpMethod(x.Trim())).ToList(); + _upstreamHttpMethod = input.Count == 0 ? new List() : input.Select(x => new HttpMethod(x.Trim())).ToList(); return this; } @@ -147,7 +147,7 @@ public DownstreamRouteBuilder WithIsCached(bool input) public DownstreamRouteBuilder WithCacheOptions(CacheOptions input) { - _fileCacheOptions = input; + _cacheOptions = input; return this; } @@ -276,7 +276,7 @@ public DownstreamRoute Build() _downstreamScheme, _requestIdHeaderKey, _isCached, - _fileCacheOptions, + _cacheOptions, _loadBalancerOptions, _rateLimitOptions, _routeClaimRequirement, diff --git a/src/Ocelot/Configuration/CacheOptions.cs b/src/Ocelot/Configuration/CacheOptions.cs index 352b501d87..dc3c191162 100644 --- a/src/Ocelot/Configuration/CacheOptions.cs +++ b/src/Ocelot/Configuration/CacheOptions.cs @@ -1,39 +1,42 @@ using Ocelot.Request.Middleware; -namespace Ocelot.Configuration -{ - public class CacheOptions - { - internal CacheOptions() { } +namespace Ocelot.Configuration; - public CacheOptions(int ttlSeconds, string region, string header) - { - TtlSeconds = ttlSeconds; - Region = region; - Header = header; - } - - public CacheOptions(int ttlSeconds, string region, string header, bool enableContentHashing) - { - TtlSeconds = ttlSeconds; - Region = region; - Header = header; - EnableContentHashing = enableContentHashing; - } +public class CacheOptions +{ + internal CacheOptions() { } - public int TtlSeconds { get; } - public string Region { get; } - public string Header { get; } - - /// - /// Enables MD5 hash calculation of the of the object. - /// - /// - /// Default value is . No hashing by default. - /// - /// - /// if hashing is enabled, otherwise it is . - /// - public bool EnableContentHashing { get; } + /// + /// Initializes a new instance of the class. + /// + /// + /// Internal defaults: + /// + /// The default value for is , but it is set to null for route-level configuration to allow global configuration usage. + /// The default value for is 0. + /// + /// + /// Time-to-live seconds. If not speciefied, zero value is used by default. + /// The region of caching. + /// The header name to control cached value. + /// The switcher for content hashing. If not speciefied, false value is used by default. + public CacheOptions(int? ttlSeconds, string region, string header, bool? enableContentHashing) + { + TtlSeconds = ttlSeconds ?? 0; + Region = region; + Header = header; + EnableContentHashing = enableContentHashing ?? false; } -} + + /// Time-to-live seconds. + /// Default value is 0. No caching by default. + /// An value of seconds. + public int TtlSeconds { get; } + public string Region { get; } + public string Header { get; } + + /// Enables MD5 hash calculation of the of the object. + /// Default value is . No hashing by default. + /// if hashing is enabled, otherwise it is . + public bool EnableContentHashing { get; } +} diff --git a/src/Ocelot/Configuration/Creator/AggregatesCreator.cs b/src/Ocelot/Configuration/Creator/AggregatesCreator.cs index 6743edfc71..9a5ae29069 100644 --- a/src/Ocelot/Configuration/Creator/AggregatesCreator.cs +++ b/src/Ocelot/Configuration/Creator/AggregatesCreator.cs @@ -24,16 +24,15 @@ private Route SetUpAggregateRoute(IEnumerable routes, FileAggregateRoute { var applicableRoutes = new List(); var allRoutes = routes.SelectMany(x => x.DownstreamRoute); - - foreach (var routeKey in aggregateRoute.RouteKeys) - { - var selec = allRoutes.FirstOrDefault(q => q.Key == routeKey); - if (selec == null) - { - return null; - } - - applicableRoutes.Add(selec); + var downstreamRoutes = aggregateRoute.RouteKeys.Select(routeKey => allRoutes.FirstOrDefault(q => q.Key == routeKey)); + foreach (var downstreamRoute in downstreamRoutes) + { + if (downstreamRoute == null) + { + return null; + } + + applicableRoutes.Add(downstreamRoute); } var upstreamTemplatePattern = _creator.Create(aggregateRoute); diff --git a/src/Ocelot/Configuration/Creator/CacheOptionsCreator.cs b/src/Ocelot/Configuration/Creator/CacheOptionsCreator.cs new file mode 100644 index 0000000000..6d1c8f2b49 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/CacheOptionsCreator.cs @@ -0,0 +1,27 @@ +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator; + +public class CacheOptionsCreator : ICacheOptionsCreator +{ + public CacheOptions Create(FileCacheOptions options, FileGlobalConfiguration global, string upstreamPathTemplate, IList upstreamHttpMethods) + { + var region = GetRegion(options.Region ?? global?.CacheOptions.Region, upstreamPathTemplate, upstreamHttpMethods); + var header = options.Header ?? global?.CacheOptions.Header; + var ttlSeconds = options.TtlSeconds ?? global?.CacheOptions.TtlSeconds; + var enableContentHashing = options.EnableContentHashing ?? global?.CacheOptions.EnableContentHashing; + + return new CacheOptions(ttlSeconds, region, header, enableContentHashing); + } + + protected virtual string GetRegion(string region, string upstreamPathTemplate, IList upstreamHttpMethod) + { + if (!string.IsNullOrEmpty(region)) + { + return region; + } + + var methods = string.Join(string.Empty, upstreamHttpMethod); + return $"{methods}{upstreamPathTemplate.Replace("/", string.Empty)}"; + } +} diff --git a/src/Ocelot/Configuration/Creator/ICacheOptionsCreator.cs b/src/Ocelot/Configuration/Creator/ICacheOptionsCreator.cs new file mode 100644 index 0000000000..a76a1b20e9 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/ICacheOptionsCreator.cs @@ -0,0 +1,19 @@ +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator; + +/// +/// This interface is used to create cache options. +/// +public interface ICacheOptionsCreator +{ + /// + /// Creates cache options based on the file cache options, upstream path template and upstream HTTP methods. + /// Upstream path template and upstream HTTP methods are used to get the region name. + /// The file cache options. + /// The global configuration. + /// The upstream path template as string. + /// The upstream http methods as a list of strings. + /// The generated cache options. + CacheOptions Create(FileCacheOptions options, FileGlobalConfiguration global, string upstreamPathTemplate, IList upstreamHttpMethods); +} diff --git a/src/Ocelot/Configuration/Creator/RouteKeyCreator.cs b/src/Ocelot/Configuration/Creator/RouteKeyCreator.cs index 93ff6f0aa6..8bc5debc94 100644 --- a/src/Ocelot/Configuration/Creator/RouteKeyCreator.cs +++ b/src/Ocelot/Configuration/Creator/RouteKeyCreator.cs @@ -1,17 +1,86 @@ using Ocelot.Configuration.File; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.LoadBalancers; -namespace Ocelot.Configuration.Creator -{ - public class RouteKeyCreator : IRouteKeyCreator +namespace Ocelot.Configuration.Creator; + +public class RouteKeyCreator : IRouteKeyCreator +{ + /// + /// Creates the unique key based on the route properties for load balancing etc. + /// + /// + /// Key template: + /// + /// UpstreamHttpMethod|UpstreamPathTemplate|UpstreamHost|DownstreamHostAndPorts|ServiceNamespace|ServiceName|LoadBalancerType|LoadBalancerKey + /// + /// + /// The route object. + /// A object containing the key. + public string Create(FileRoute fileRoute) { - public string Create(FileRoute fileRoute) => IsStickySession(fileRoute) - ? $"{nameof(CookieStickySessions)}:{fileRoute.LoadBalancerOptions.Key}" - : $"{fileRoute.UpstreamPathTemplate}|{string.Join(',', fileRoute.UpstreamHttpMethod)}|{string.Join(',', fileRoute.DownstreamHostAndPorts.Select(x => $"{x.Host}:{x.Port}"))}"; + var isStickySession = fileRoute.LoadBalancerOptions is + { + Type: nameof(CookieStickySessions), + Key.Length: > 0 + }; + + if (isStickySession) + { + return $"{nameof(CookieStickySessions)}:{fileRoute.LoadBalancerOptions.Key}"; + } + + var upstreamHttpMethods = Csv(fileRoute.UpstreamHttpMethod); + var downstreamHostAndPorts = Csv(fileRoute.DownstreamHostAndPorts.Select(downstream => $"{downstream.Host}:{downstream.Port}")); + + var keyBuilder = new StringBuilder() + + // UpstreamHttpMethod and UpstreamPathTemplate are required + .AppendNext(upstreamHttpMethods) + .AppendNext(fileRoute.UpstreamPathTemplate) + + // Other properties are optional, replace undefined values with defaults to aid debugging + .AppendNext(Coalesce(fileRoute.UpstreamHost, "no-host")) + + .AppendNext(Coalesce(downstreamHostAndPorts, "no-host-and-port")) + .AppendNext(Coalesce(fileRoute.ServiceNamespace, "no-svc-ns")) + .AppendNext(Coalesce(fileRoute.ServiceName, "no-svc-name")) + .AppendNext(Coalesce(fileRoute.LoadBalancerOptions.Type, "no-lb-type")) + .AppendNext(Coalesce(fileRoute.LoadBalancerOptions.Key, "no-lb-key")); - private static bool IsStickySession(FileRoute fileRoute) => - !string.IsNullOrEmpty(fileRoute.LoadBalancerOptions.Type) - && !string.IsNullOrEmpty(fileRoute.LoadBalancerOptions.Key) - && fileRoute.LoadBalancerOptions.Type == nameof(CookieStickySessions); - } + return keyBuilder.ToString(); + } + + /// + /// Helper function to convert multiple strings into a comma-separated string. + /// + /// The collection of strings to join by comma separator. + /// A in the comma-separated format. + private static string Csv(IEnumerable values) => string.Join(',', values); + + /// + /// Helper function to return the first non-null-or-whitespace string. + /// + /// The 1st string to check. + /// The 2nd string to check. + /// A which is not empty. + private static string Coalesce(string first, string second) => string.IsNullOrWhiteSpace(first) ? second : first; +} + +internal static class RouteKeyCreatorHelpers +{ + /// + /// Helper function to append a string to the key builder, separated by a pipe. + /// + /// The builder of the key. + /// The next word to add. + /// The reference to the builder. + public static StringBuilder AppendNext(this StringBuilder builder, string next) + { + if (builder.Length > 0) + { + builder.Append('|'); + } + + return builder.Append(next); + } } diff --git a/src/Ocelot/Configuration/Creator/RouteOptionsCreator.cs b/src/Ocelot/Configuration/Creator/RouteOptionsCreator.cs index 8e0911e560..8b6f0e3eac 100644 --- a/src/Ocelot/Configuration/Creator/RouteOptionsCreator.cs +++ b/src/Ocelot/Configuration/Creator/RouteOptionsCreator.cs @@ -17,6 +17,8 @@ public RouteOptions Create(FileRoute fileRoute) && (!string.IsNullOrEmpty(authOpts.AuthenticationProviderKey) || authOpts.AuthenticationProviderKeys?.Any(k => !string.IsNullOrWhiteSpace(k)) == true); var isAuthorized = fileRoute.RouteClaimsRequirement?.Any() == true; + + // TODO: This sounds more like a hack, it might be better to refactor this at some point. var isCached = fileRoute.FileCacheOptions.TtlSeconds > 0; var enableRateLimiting = fileRoute.RateLimitOptions?.EnableRateLimiting == true; var useServiceDiscovery = !string.IsNullOrEmpty(fileRoute.ServiceName); diff --git a/src/Ocelot/Configuration/Creator/RoutesCreator.cs b/src/Ocelot/Configuration/Creator/RoutesCreator.cs index 8c1f1de639..19bce7b17e 100644 --- a/src/Ocelot/Configuration/Creator/RoutesCreator.cs +++ b/src/Ocelot/Configuration/Creator/RoutesCreator.cs @@ -1,5 +1,4 @@ -using Ocelot.Cache; -using Ocelot.Configuration.Builder; +using Ocelot.Configuration.Builder; using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator @@ -14,7 +13,7 @@ public class RoutesCreator : IRoutesCreator private readonly IQoSOptionsCreator _qosOptionsCreator; private readonly IRouteOptionsCreator _fileRouteOptionsCreator; private readonly IRateLimitOptionsCreator _rateLimitOptionsCreator; - private readonly IRegionCreator _regionCreator; + private readonly ICacheOptionsCreator _cacheOptionsCreator; private readonly IHttpHandlerOptionsCreator _httpHandlerOptionsCreator; private readonly IHeaderFindAndReplaceCreator _headerFAndRCreator; private readonly IDownstreamAddressesCreator _downstreamAddressesCreator; @@ -30,21 +29,20 @@ public RoutesCreator( IQoSOptionsCreator qosOptionsCreator, IRouteOptionsCreator fileRouteOptionsCreator, IRateLimitOptionsCreator rateLimitOptionsCreator, - IRegionCreator regionCreator, + ICacheOptionsCreator cacheOptionsCreator, IHttpHandlerOptionsCreator httpHandlerOptionsCreator, IHeaderFindAndReplaceCreator headerFAndRCreator, IDownstreamAddressesCreator downstreamAddressesCreator, ILoadBalancerOptionsCreator loadBalancerOptionsCreator, IRouteKeyCreator routeKeyCreator, ISecurityOptionsCreator securityOptionsCreator, - IVersionCreator versionCreator - ) + IVersionCreator versionCreator) { _routeKeyCreator = routeKeyCreator; _loadBalancerOptionsCreator = loadBalancerOptionsCreator; _downstreamAddressesCreator = downstreamAddressesCreator; _headerFAndRCreator = headerFAndRCreator; - _regionCreator = regionCreator; + _cacheOptionsCreator = cacheOptionsCreator; _rateLimitOptionsCreator = rateLimitOptionsCreator; _requestIdKeyCreator = requestIdKeyCreator; _upstreamTemplatePatternCreator = upstreamTemplatePatternCreator; @@ -93,8 +91,6 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf var rateLimitOption = _rateLimitOptionsCreator.Create(fileRoute.RateLimitOptions, globalConfiguration); - var region = _regionCreator.Create(fileRoute); - var httpHandlerOptions = _httpHandlerOptionsCreator.Create(fileRoute.HttpHandlerOptions); var hAndRs = _headerFAndRCreator.Create(fileRoute); @@ -107,6 +103,8 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf var downstreamHttpVersion = _versionCreator.Create(fileRoute.DownstreamHttpVersion); + var cacheOptions = _cacheOptionsCreator.Create(fileRoute.FileCacheOptions, globalConfiguration, fileRoute.UpstreamPathTemplate, fileRoute.UpstreamHttpMethod); + var route = new DownstreamRouteBuilder() .WithKey(fileRoute.Key) .WithDownstreamPathTemplate(fileRoute.DownstreamPathTemplate) @@ -122,7 +120,7 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf .WithClaimsToDownstreamPath(claimsToDownstreamPath) .WithRequestIdKey(requestIdKey) .WithIsCached(fileRouteOptions.IsCached) - .WithCacheOptions(new CacheOptions(fileRoute.FileCacheOptions.TtlSeconds, region, fileRoute.FileCacheOptions.Header)) + .WithCacheOptions(cacheOptions) .WithDownstreamScheme(fileRoute.DownstreamScheme) .WithLoadBalancerOptions(lbOptions) .WithDownstreamAddresses(downstreamAddresses) diff --git a/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs b/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs index 24d9b787de..0dd93a9c61 100644 --- a/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs +++ b/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs @@ -4,13 +4,13 @@ public sealed class FileAuthenticationOptions { public FileAuthenticationOptions() { - AllowedScopes = []; - AuthenticationProviderKeys = []; + AllowedScopes = new(); + AuthenticationProviderKeys = Array.Empty(); } public FileAuthenticationOptions(FileAuthenticationOptions from) { - AllowedScopes = [..from.AllowedScopes]; + AllowedScopes = new(from.AllowedScopes); AuthenticationProviderKey = from.AuthenticationProviderKey; AuthenticationProviderKeys = from.AuthenticationProviderKeys; } diff --git a/src/Ocelot/Configuration/File/FileCacheOptions.cs b/src/Ocelot/Configuration/File/FileCacheOptions.cs index a1b1deed5b..42b793390e 100644 --- a/src/Ocelot/Configuration/File/FileCacheOptions.cs +++ b/src/Ocelot/Configuration/File/FileCacheOptions.cs @@ -1,21 +1,26 @@ -namespace Ocelot.Configuration.File -{ - public class FileCacheOptions - { - public FileCacheOptions() - { - Region = string.Empty; - TtlSeconds = 0; - } +namespace Ocelot.Configuration.File; - public FileCacheOptions(FileCacheOptions from) - { - Region = from.Region; - TtlSeconds = from.TtlSeconds; - } +public class FileCacheOptions +{ + public FileCacheOptions() { } - public int TtlSeconds { get; set; } - public string Region { get; set; } - public string Header { get; set; } - } + public FileCacheOptions(FileCacheOptions from) + { + Region = from.Region; + TtlSeconds = from.TtlSeconds; + Header = from.Header; + EnableContentHashing = from.EnableContentHashing; + } + + /// Using where T is to have as default value and allowing global configuration usage. + /// If then use global configuration with 0 by default. + /// The time to live seconds, with 0 by default. + public int? TtlSeconds { get; set; } + public string Region { get; set; } + public string Header { get; set; } + + /// Using where T is to have as default value and allowing global configuration usage. + /// If then use global configuration with by default. + /// if content hashing is enabled; otherwise, . + public bool? EnableContentHashing { get; set; } } diff --git a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs index 6692ba046b..04497cca18 100644 --- a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs +++ b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs @@ -9,6 +9,7 @@ public FileGlobalConfiguration() LoadBalancerOptions = new FileLoadBalancerOptions(); QoSOptions = new FileQoSOptions(); HttpHandlerOptions = new FileHttpHandlerOptions(); + CacheOptions = new FileCacheOptions(); } public string RequestIdKey { get; set; } @@ -28,5 +29,7 @@ public FileGlobalConfiguration() public FileHttpHandlerOptions HttpHandlerOptions { get; set; } public string DownstreamHttpVersion { get; set; } + + public FileCacheOptions CacheOptions { get; set; } } } diff --git a/src/Ocelot/Configuration/File/FileRoute.cs b/src/Ocelot/Configuration/File/FileRoute.cs index 5823113ad9..ea076e0223 100644 --- a/src/Ocelot/Configuration/File/FileRoute.cs +++ b/src/Ocelot/Configuration/File/FileRoute.cs @@ -41,8 +41,8 @@ public FileRoute(FileRoute from) public string DownstreamHttpMethod { get; set; } public string DownstreamHttpVersion { get; set; } public string DownstreamPathTemplate { get; set; } - public string DownstreamScheme { get; set; } - public FileCacheOptions FileCacheOptions { get; set; } + public string DownstreamScheme { get; set; } + public FileCacheOptions FileCacheOptions { get; set; } public FileHttpHandlerOptions HttpHandlerOptions { get; set; } public string Key { get; set; } public FileLoadBalancerOptions LoadBalancerOptions { get; set; } diff --git a/src/Ocelot/Configuration/QoSOptions.cs b/src/Ocelot/Configuration/QoSOptions.cs index 915f07e2a0..e1897bc580 100644 --- a/src/Ocelot/Configuration/QoSOptions.cs +++ b/src/Ocelot/Configuration/QoSOptions.cs @@ -32,10 +32,41 @@ public QoSOptions( TimeoutValue = timeoutValue; } + /// + /// How long the circuit should stay open before resetting in milliseconds. + /// + /// + /// If using Polly version 8 or above, this value must be 500 (0.5 sec) or greater. + /// + /// + /// An value (milliseconds). + /// public int DurationOfBreak { get; } + + /// + /// How many times a circuit can fail before being set to open. + /// + /// + /// If using Polly version 8 or above, this value must be 2 or greater. + /// + /// + /// An value (no of exceptions). + /// public int ExceptionsAllowedBeforeBreaking { get; } + public string Key { get; } + + /// + /// Value for TimeoutStrategy in milliseconds. + /// + /// + /// If using Polly version 8 or above, this value must be 1000 (1 sec) or greater. + /// + /// + /// An value (milliseconds). + /// public int TimeoutValue { get; } + public bool UseQos => ExceptionsAllowedBeforeBreaking > 0 || TimeoutValue > 0; } } diff --git a/src/Ocelot/Configuration/Repository/DiskFileConfigurationRepository.cs b/src/Ocelot/Configuration/Repository/DiskFileConfigurationRepository.cs index 18fe36ee51..988292b94e 100644 --- a/src/Ocelot/Configuration/Repository/DiskFileConfigurationRepository.cs +++ b/src/Ocelot/Configuration/Repository/DiskFileConfigurationRepository.cs @@ -2,37 +2,53 @@ using Newtonsoft.Json; using Ocelot.Configuration.ChangeTracking; using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; using Ocelot.Responses; +using FileSys = System.IO.File; namespace Ocelot.Configuration.Repository { public class DiskFileConfigurationRepository : IFileConfigurationRepository { + private readonly IWebHostEnvironment _hostingEnvironment; private readonly IOcelotConfigurationChangeTokenSource _changeTokenSource; - private readonly string _environmentFilePath; - private readonly string _ocelotFilePath; - private static readonly object _lock = new(); - - /// - /// Main prefix for Ocelot configuration JSON-files. - /// - public const string ConfigurationFileName = "ocelot"; + private FileInfo _ocelotFile; + private FileInfo _environmentFile; + private readonly object _lock = new(); public DiskFileConfigurationRepository(IWebHostEnvironment hosting, IOcelotConfigurationChangeTokenSource changeTokenSource) { + _hostingEnvironment = hostingEnvironment; _changeTokenSource = changeTokenSource; _environmentFilePath = $"{AppContext.BaseDirectory}{ConfigurationFileName}{(string.IsNullOrEmpty(hosting.EnvironmentName) ? string.Empty : ".")}{hosting.EnvironmentName}.json"; + Initialize(AppContext.BaseDirectory); + } + + public DiskFileConfigurationRepository(IWebHostEnvironment hostingEnvironment, IOcelotConfigurationChangeTokenSource changeTokenSource, string folder) + { + _hostingEnvironment = hostingEnvironment; + _changeTokenSource = changeTokenSource; + Initialize(folder); + } - _ocelotFilePath = $"{AppContext.BaseDirectory}{ConfigurationFileName}.json"; + private void Initialize(string folder) + { + folder ??= AppContext.BaseDirectory; + _ocelotFile = new FileInfo(Path.Combine(folder, ConfigurationBuilderExtensions.PrimaryConfigFile)); + var envFile = !string.IsNullOrEmpty(_hostingEnvironment.EnvironmentName) + ? string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, _hostingEnvironment.EnvironmentName) + : ConfigurationBuilderExtensions.PrimaryConfigFile; + _environmentFile = new FileInfo(Path.Combine(folder, envFile)); } + public Task> Get() public Task GetAsync() { string jsonConfiguration; lock (_lock) { - jsonConfiguration = System.IO.File.ReadAllText(_environmentFilePath); + jsonConfiguration = FileSys.ReadAllText(_environmentFile.FullName); } var fileConfiguration = JsonConvert.DeserializeObject(jsonConfiguration); @@ -45,19 +61,19 @@ public Task SetAsync(FileConfiguration fileConfiguration) lock (_lock) { - if (System.IO.File.Exists(_environmentFilePath)) + if (_environmentFile.Exists) { - System.IO.File.Delete(_environmentFilePath); + _environmentFile.Delete(); } - System.IO.File.WriteAllText(_environmentFilePath, jsonConfiguration); + FileSys.WriteAllText(_environmentFile.FullName, jsonConfiguration); - if (System.IO.File.Exists(_ocelotFilePath)) + if (_ocelotFile.Exists) { - System.IO.File.Delete(_ocelotFilePath); + _ocelotFile.Delete(); } - System.IO.File.WriteAllText(_ocelotFilePath, jsonConfiguration); + FileSys.WriteAllText(_ocelotFile.FullName, jsonConfiguration); } _changeTokenSource.Activate(); diff --git a/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs b/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs index 064a4e0c9a..7e74251e23 100644 --- a/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs +++ b/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs @@ -7,10 +7,13 @@ namespace Ocelot.Configuration.Validator { - public class FileConfigurationFluentValidator : AbstractValidator, IConfigurationValidator + /// + /// Validation of a objects. + /// + public partial class FileConfigurationFluentValidator : AbstractValidator, IConfigurationValidator { private const string Servicefabric = "servicefabric"; - private readonly List _serviceDiscoveryFinderDelegates; + private readonly List _serviceDiscoveryFinderDelegates; public FileConfigurationFluentValidator(IServiceProvider provider, RouteFluentValidator routeFluentValidator, FileGlobalConfigurationFluentValidator fileGlobalConfigurationFluentValidator) { @@ -34,7 +37,10 @@ public FileConfigurationFluentValidator(IServiceProvider provider, RouteFluentVa RuleForEach(configuration => configuration.Routes) .Must((_, route) => IsPlaceholderNotDuplicatedIn(route.UpstreamPathTemplate)) - .WithMessage((_, route) => $"{nameof(route)} {route.UpstreamPathTemplate} has duplicated placeholder"); + .WithMessage((_, route) => $"{nameof(route.UpstreamPathTemplate)} '{route.UpstreamPathTemplate}' has duplicated placeholder"); + RuleForEach(configuration => configuration.Routes) + .Must((_, route) => IsPlaceholderNotDuplicatedIn(route.DownstreamPathTemplate)) + .WithMessage((_, route) => $"{nameof(route.DownstreamPathTemplate)} '{route.DownstreamPathTemplate}' has duplicated placeholder"); RuleFor(configuration => configuration.GlobalConfiguration.ServiceDiscoveryProvider) .Must(HaveServiceDiscoveryProviderRegistered) @@ -93,15 +99,22 @@ private static bool AllRoutesForAggregateExist(FileAggregateRoute fileAggregateR return routesForAggregate.Count() == fileAggregateRoute.RouteKeys.Count; } - - private static bool IsPlaceholderNotDuplicatedIn(string upstreamPathTemplate) + +#if NET7_0_OR_GREATER + [GeneratedRegex(@"\{\w+\}", RegexOptions.IgnoreCase | RegexOptions.Singleline, "en-US")] + private static partial Regex PlaceholderRegex(); +#else + private static readonly Regex PlaceholderRegexVar = new(@"\{\w+\}", RegexOptions.IgnoreCase | RegexOptions.Singleline, TimeSpan.FromMilliseconds(1000)); + private static Regex PlaceholderRegex() => PlaceholderRegexVar; +#endif + + private static bool IsPlaceholderNotDuplicatedIn(string pathTemplate) { - var regExPlaceholder = new Regex("{[^}]+}"); - var matches = regExPlaceholder.Matches(upstreamPathTemplate); - var upstreamPathPlaceholders = matches.Select(m => m.Value); - return upstreamPathPlaceholders.Count() == upstreamPathPlaceholders.Distinct().Count(); - } - + var placeholders = PlaceholderRegex().Matches(pathTemplate) + .Select(m => m.Value).ToList(); + return placeholders.Count == placeholders.Distinct().Count(); + } + private static bool DoesNotContainRoutesWithSpecificRequestIdKeys(FileAggregateRoute fileAggregateRoute, IEnumerable routes) { @@ -131,7 +144,7 @@ private static bool IsNotDuplicateIn(FileRoute route, var duplicateSpecificVerbs = matchingRoutes.SelectMany(x => x.UpstreamHttpMethod).GroupBy(x => x.ToLower()).SelectMany(x => x.Skip(1)).Any(); - if (duplicateAllowAllVerbs || duplicateSpecificVerbs || (allowAllVerbs && specificVerbs)) + if (duplicateAllowAllVerbs || duplicateSpecificVerbs || allowAllVerbs && specificVerbs) { return false; } @@ -155,6 +168,6 @@ private static bool IsNotDuplicateIn(FileAggregateRoute route, IEnumerable r.UpstreamPathTemplate == route.UpstreamPathTemplate & r.UpstreamHost == route.UpstreamHost); return matchingRoutes.Count() <= 1; - } + } } } diff --git a/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs b/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs index 3af7b58c7f..909b57d572 100644 --- a/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs +++ b/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs @@ -14,16 +14,14 @@ public static partial class ConfigurationBuilderExtensions { public const string PrimaryConfigFile = "ocelot.json"; public const string GlobalConfigFile = "ocelot.global.json"; + public const string EnvironmentConfigFile = "ocelot.{0}.json"; #if NET7_0_OR_GREATER [GeneratedRegex(@"^ocelot\.(.*?)\.json$", RegexOptions.IgnoreCase | RegexOptions.Singleline, "en-US")] private static partial Regex SubConfigRegex(); #else private static readonly Regex SubConfigRegexVar = new(@"^ocelot\.(.*?)\.json$", RegexOptions.IgnoreCase | RegexOptions.Singleline, TimeSpan.FromMilliseconds(1000)); - private static Regex SubConfigRegex() - { - return SubConfigRegexVar; - } + private static Regex SubConfigRegex() => SubConfigRegexVar; #endif [Obsolete("Please set BaseUrl in ocelot.json GlobalConfiguration.BaseUrl")] @@ -37,9 +35,7 @@ public static IConfigurationBuilder AddOcelotBaseUrl(this IConfigurationBuilder }, }; - builder.Add(memorySource); - - return builder; + return builder.Add(memorySource); } /// @@ -49,9 +45,7 @@ public static IConfigurationBuilder AddOcelotBaseUrl(this IConfigurationBuilder /// Web hosting environment object. /// An object. public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, IWebHostEnvironment env) - { - return builder.AddOcelot(".", env); - } + => builder.AddOcelot(".", env); /// /// Adds Ocelot configuration by environment, reading the required files from the specified folder. @@ -61,36 +55,94 @@ public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder /// Web hosting environment object. /// An object. public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, string folder, IWebHostEnvironment env) + => builder.AddOcelot(folder, env, MergeOcelotJson.ToFile); + + /// + /// Adds Ocelot configuration by environment and merge option, reading the required files from the current default folder. + /// + /// Use optional arguments for injections and overridings. + /// Configuration builder to extend. + /// Web hosting environment object. + /// Option to merge files to. + /// Primary config file. + /// Global config file. + /// Environment config file. + /// The 2nd argument of the AddJsonFile. + /// The 3rd argument of the AddJsonFile. + /// An object. + public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, IWebHostEnvironment env, MergeOcelotJson mergeTo, + string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections + => builder.AddOcelot(".", env, mergeTo, primaryConfigFile, globalConfigFile, environmentConfigFile, optional, reloadOnChange); + + /// + /// Adds Ocelot configuration by environment and merge option, reading the required files from the specified folder. + /// + /// Use optional arguments for injections and overridings. + /// Configuration builder to extend. + /// Folder to read files from. + /// Web hosting environment object. + /// Option to merge files to. + /// Primary config file. + /// Global config file. + /// Environment config file. + /// The 2nd argument of the AddJsonFile. + /// The 3rd argument of the AddJsonFile. + /// An object. + public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, string folder, IWebHostEnvironment env, MergeOcelotJson mergeTo, + string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections { - var excludeConfigName = env?.EnvironmentName != null ? $"ocelot.{env.EnvironmentName}.json" : string.Empty; + var json = GetMergedOcelotJson(folder, env, null, primaryConfigFile, globalConfigFile, environmentConfigFile); + return ApplyMergeOcelotJsonOption(builder, mergeTo, json, primaryConfigFile, optional, reloadOnChange); + } - var reg = SubConfigRegex(); + private static IConfigurationBuilder ApplyMergeOcelotJsonOption(IConfigurationBuilder builder, MergeOcelotJson mergeTo, string json, + string primaryConfigFile, bool? optional, bool? reloadOnChange) + { + return mergeTo == MergeOcelotJson.ToMemory ? + builder.AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) : + AddOcelotJsonFile(builder, json, primaryConfigFile, optional, reloadOnChange); + } + private static string GetMergedOcelotJson(string folder, IWebHostEnvironment env, + FileConfiguration fileConfiguration = null, string primaryFile = null, string globalFile = null, string environmentFile = null) + { + var envName = string.IsNullOrEmpty(env?.EnvironmentName) ? "Development" : env.EnvironmentName; + environmentFile ??= string.Format(EnvironmentConfigFile, envName); + var reg = SubConfigRegex(); + var environmentFileInfo = new FileInfo(environmentFile); var files = new DirectoryInfo(folder) .EnumerateFiles() - .Where(fi => reg.IsMatch(fi.Name) && fi.Name != excludeConfigName) + .Where(fi => reg.IsMatch(fi.Name) && + !fi.Name.Equals(environmentFileInfo.Name, StringComparison.OrdinalIgnoreCase) && + !fi.FullName.Equals(environmentFileInfo.FullName, StringComparison.OrdinalIgnoreCase)) .ToArray(); - dynamic fileConfiguration = new JObject(); - fileConfiguration.GlobalConfiguration = new JObject(); - fileConfiguration.Aggregates = new JArray(); - fileConfiguration.Routes = new JArray(); - + dynamic fileConfiguration = new JObject(); + fileConfiguration.GlobalConfiguration = new JObject(); + fileConfiguration.Aggregates = new JArray(); + fileConfiguration.Routes = new JArray(); + primaryFile ??= PrimaryConfigFile; + globalFile ??= GlobalConfigFile; + var primaryFileInfo = new FileInfo(primaryFile); + var globalFileInfo = new FileInfo(globalFile); foreach (var file in files) { - if (files.Length > 1 && file.Name.Equals(PrimaryConfigFile, StringComparison.OrdinalIgnoreCase)) + if (files.Length > 1 && + file.Name.Equals(primaryFileInfo.Name, StringComparison.OrdinalIgnoreCase) && + file.FullName.Equals(primaryFileInfo.FullName, StringComparison.OrdinalIgnoreCase)) { continue; } var lines = File.ReadAllText(file.FullName); - dynamic config = JToken.Parse(lines); - var isGlobal = file.Name.Equals(GlobalConfigFile, StringComparison.OrdinalIgnoreCase); - - MergeConfig(fileConfiguration, config, isGlobal); + dynamic config = JToken.Parse(lines); + var isGlobal = file.Name.Equals(globalFileInfo.Name, StringComparison.OrdinalIgnoreCase) && + file.FullName.Equals(globalFileInfo.FullName, StringComparison.OrdinalIgnoreCase); + + MergeConfig(fileConfiguration, config, isGlobal); } - return AddOcelot(builder, (JObject)fileConfiguration); + return (JObject)fileConfiguration.ToString(); } public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, JObject fileConfiguration) @@ -100,17 +152,59 @@ public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder /// Adds Ocelot configuration by ready configuration object and writes JSON to the primary configuration file.
/// Finally, adds JSON file as configuration provider. ///
+ /// Use optional arguments for injections and overridings. /// Configuration builder to extend. /// File configuration to add as JSON provider. + /// Primary config file. + /// The 2nd argument of the AddJsonFile. + /// The 3rd argument of the AddJsonFile. /// An object. - public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration) + public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration, + string primaryConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections => SerializeToFile(builder, fileConfiguration); private static IConfigurationBuilder SerializeToFile(IConfigurationBuilder builder, object fileConfiguration) { - var json = JsonConvert.SerializeObject(fileConfiguration); - File.WriteAllText(PrimaryConfigFile, json); - return builder.AddJsonFile(PrimaryConfigFile, false, false); + var json = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); + return AddOcelotJsonFile(builder, json, primaryConfigFile, optional, reloadOnChange); + } + /// + /// Adds Ocelot configuration by ready configuration object, environment and merge option, reading the required files from the current default folder. + /// + /// Configuration builder to extend. + /// File configuration to add as JSON provider. + /// Web hosting environment object. + /// Option to merge files to. + /// Primary config file. + /// Global config file. + /// Environment config file. + /// The 2nd argument of the AddJsonFile. + /// The 3rd argument of the AddJsonFile. + /// An object. + public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration, IWebHostEnvironment env, MergeOcelotJson mergeTo, + string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections + { + var json = GetMergedOcelotJson(".", env, fileConfiguration, primaryConfigFile, globalConfigFile, environmentConfigFile); + return ApplyMergeOcelotJsonOption(builder, mergeTo, json, primaryConfigFile, optional, reloadOnChange); + } + /// + /// Adds Ocelot primary configuration file (aka ocelot.json).
+ /// Writes JSON to the file.
+ /// Adds the file as a JSON configuration provider via the extension. + ///
+ /// Use optional arguments for injections and overridings. + /// The builder to extend. + /// JSON data of the Ocelot configuration. + /// Primary config file. + /// The 2nd argument of the AddJsonFile. + /// The 3rd argument of the AddJsonFile. + /// An object. + private static IConfigurationBuilder AddOcelotJsonFile(IConfigurationBuilder builder, string json, + string primaryFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections + { + var primary = primaryFile ?? PrimaryConfigFile; + File.WriteAllText(primary, json); + return builder?.AddJsonFile(primary, optional ?? false, reloadOnChange ?? false); } private static void MergeConfig(JToken destConfig, JToken srcConfig, bool isGlobal) diff --git a/src/Ocelot/DependencyInjection/Features.cs b/src/Ocelot/DependencyInjection/Features.cs new file mode 100644 index 0000000000..8910145afd --- /dev/null +++ b/src/Ocelot/DependencyInjection/Features.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Cache; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.RateLimiting; + +namespace Ocelot.DependencyInjection; + +public static class Features +{ + /// + /// Ocelot feature: Rate Limiting. + /// + /// + /// Read The Docs: Rate Limiting. + /// + /// The services collection to add the feature to. + /// The same object. + public static IServiceCollection AddRateLimiting(this IServiceCollection services) => services + .AddSingleton() + .AddSingleton(); + + /// + /// Ocelot feature: Request Caching. + /// + /// + /// Read The Docs: Caching. + /// + /// The services collection to add the feature to. + /// The same object. + public static IServiceCollection AddOcelotCache(this IServiceCollection services) => services + .AddSingleton, DefaultMemoryCache>() + .AddSingleton, DefaultMemoryCache>() + .AddSingleton() + .AddSingleton(); +} diff --git a/src/Ocelot/DependencyInjection/MergeOcelotJson.cs b/src/Ocelot/DependencyInjection/MergeOcelotJson.cs new file mode 100644 index 0000000000..7d7de5e623 --- /dev/null +++ b/src/Ocelot/DependencyInjection/MergeOcelotJson.cs @@ -0,0 +1,14 @@ +namespace Ocelot.DependencyInjection; + +public enum MergeOcelotJson +{ + /// + /// The option to merge all configuration files to one primary config file aka ocelot.json. + /// + ToFile = 0, + + /// + /// The option to merge all configuration files to memory and reuse the config by in-memory configuration provider. + /// + ToMemory = 1, +} diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index a72ec3cbfa..9dc65a3de9 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -27,7 +27,7 @@ using Ocelot.Multiplexer; using Ocelot.PathManipulation; using Ocelot.QueryStrings; -using Ocelot.RateLimit; +using Ocelot.RateLimiting; using Ocelot.Request.Creator; using Ocelot.Request.Mapper; using Ocelot.Requester; @@ -54,8 +54,6 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services = services; Services.Configure(configurationRoot); - Services.TryAddSingleton, AspMemoryCache>(); - Services.TryAddSingleton, AspMemoryCache>(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); @@ -81,7 +79,6 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); - Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); @@ -109,14 +106,16 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); - Services.TryAddSingleton(); + Services.AddRateLimiting(); // Feature: Rate Limiting Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); - Services.TryAddSingleton(); + Services.TryAddSingleton(); Services.TryAddSingleton, OcelotConfigurationMonitor>(); + + Services.AddOcelotCache(); Services.AddOcelotMessageInvokerPool(); // See this for why we register this as singleton: diff --git a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs index 9818a62e6a..864419f775 100644 --- a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,5 +1,7 @@ -using Microsoft.Extensions.Configuration; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using System.Reflection; namespace Ocelot.DependencyInjection; @@ -8,7 +10,8 @@ public static class ServiceCollectionExtensions { /// /// Adds default ASP.NET services and Ocelot application services.
- /// Creates default object via the extension-method. + /// Creates default from the current service descriptors. + /// If the configuration is not registered, it will try to read ocelot configuration from current working directory. ///
/// /// Remarks for default ASP.NET services being injected see in docs of the method. @@ -17,7 +20,7 @@ public static class ServiceCollectionExtensions /// An object. public static IOcelotBuilder AddOcelot(this IServiceCollection services) { - var configuration = services.BuildServiceProvider().GetRequiredService(); + var configuration = services.FindConfiguration(null); return new OcelotBuilder(services, configuration); } @@ -25,7 +28,7 @@ public static IOcelotBuilder AddOcelot(this IServiceCollection services) /// Adds default ASP.NET services and Ocelot application services with configuration. ///
/// - /// Remarks for default ASP.NET services being injected see in docs of the method. + /// Remarks for default ASP.NET services will be injected, see docs of the method. /// /// Current services collection. /// Current web app configuration. @@ -37,7 +40,8 @@ public static IOcelotBuilder AddOcelot(this IServiceCollection services, IConfig /// /// Adds Ocelot application services and custom ASP.NET services with custom builder.
- /// Creates default object via the extension-method. + /// Creates default from the current service descriptors. + /// If the configuration is not registered, it will try to read ocelot configuration from current working directory. ///
/// /// Warning! To understand which ASP.NET services should be injected/removed by custom builder, see docs of the method. @@ -45,9 +49,10 @@ public static IOcelotBuilder AddOcelot(this IServiceCollection services, IConfig /// Current services collection. /// Current custom builder for ASP.NET MVC pipeline. /// An object. + [Obsolete("Use AddOcelotUsingBuilder() overloaded version with the 'IConfiguration configuration' parameter.")] public static IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, Func customBuilder) { - var configuration = services.BuildServiceProvider().GetRequiredService(); + var configuration = services.FindConfiguration(null); return new OcelotBuilder(services, configuration, customBuilder); } @@ -63,6 +68,23 @@ public static IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection servi /// An object. public static IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, IConfiguration configuration, Func customBuilder) { + configuration ??= services.FindConfiguration(null); return new OcelotBuilder(services, configuration, customBuilder); } + + private static IConfiguration DefaultConfiguration(IWebHostEnvironment env) + => new ConfigurationBuilder().AddOcelot(env).Build(); + + private static IConfiguration FindConfiguration(this IServiceCollection services, IWebHostEnvironment env) + { + var descriptor = services.FirstOrDefault(descriptor => descriptor.ServiceType == typeof(IConfiguration)); + if (descriptor == null) + { + return DefaultConfiguration(env); + } + + var provider = new ServiceCollection().Add(descriptor).BuildServiceProvider(); + var configuration = provider.GetService(); + return configuration ?? DefaultConfiguration(env); + } } diff --git a/src/Ocelot/Errors/OcelotErrorCode.cs b/src/Ocelot/Errors/OcelotErrorCode.cs index 9063e71426..f74cdace19 100644 --- a/src/Ocelot/Errors/OcelotErrorCode.cs +++ b/src/Ocelot/Errors/OcelotErrorCode.cs @@ -43,5 +43,6 @@ public enum OcelotErrorCode ConnectionToDownstreamServiceError = 38, CouldNotFindLoadBalancerCreator = 39, ErrorInvokingLoadBalancerCreator = 40, + PayloadTooLargeError = 41, } } diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs index 18a380a488..dfa6279e61 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs @@ -1,5 +1,6 @@ using Ocelot.Configuration; -using Ocelot.Responses; +using Ocelot.Errors; +using Ocelot.Responses; namespace Ocelot.LoadBalancer.LoadBalancers { @@ -18,45 +19,40 @@ public Response Get(DownstreamRoute route, ServiceProviderConfigu { try { - Response result; - if (_loadBalancers.TryGetValue(route.LoadBalancerKey, out var loadBalancer)) - { - loadBalancer = _loadBalancers[route.LoadBalancerKey]; - + { + // TODO Fix ugly reflection issue of dymanic detection in favor of static type property if (route.LoadBalancerOptions.Type != loadBalancer.GetType().Name) - { - result = _factory.Get(route, config); - if (result.IsError) - { - return new ErrorResponse(result.Errors); - } - - loadBalancer = result.Data; - AddLoadBalancer(route.LoadBalancerKey, loadBalancer); + { + return GetResponse(route, config); } return new OkResponse(loadBalancer); } - result = _factory.Get(route, config); - - if (result.IsError) - { - return new ErrorResponse(result.Errors); - } - - loadBalancer = result.Data; - AddLoadBalancer(route.LoadBalancerKey, loadBalancer); - return new OkResponse(loadBalancer); + return GetResponse(route, config); } catch (Exception ex) { - return new ErrorResponse(new List + return new ErrorResponse(new List() { - new UnableToFindLoadBalancerError($"unabe to find load balancer for {route.LoadBalancerKey} exception is {ex}"), + new UnableToFindLoadBalancerError($"Unable to find load balancer for '{route.LoadBalancerKey}'. Exception: {ex};"), }); } + } + + private Response GetResponse(DownstreamRoute route, ServiceProviderConfiguration config) + { + var result = _factory.Get(route, config); + + if (result.IsError) + { + return new ErrorResponse(result.Errors); + } + + var loadBalancer = result.Data; + AddLoadBalancer(route.LoadBalancerKey, loadBalancer); + return new OkResponse(loadBalancer); } private void AddLoadBalancer(string key, ILoadBalancer loadBalancer) diff --git a/src/Ocelot/Middleware/HttpItemsExtensions.cs b/src/Ocelot/Middleware/HttpItemsExtensions.cs index 5d2ada0a5a..df97e0a19d 100644 --- a/src/Ocelot/Middleware/HttpItemsExtensions.cs +++ b/src/Ocelot/Middleware/HttpItemsExtensions.cs @@ -56,7 +56,7 @@ public static IInternalConfiguration IInternalConfiguration(this IDictionary Errors(this IDictionary input) { var errors = input.Get>("Errors"); - return errors ?? new List(); + return errors ?? new(); } public static DownstreamRouteFinder.DownstreamRouteHolder diff --git a/src/Ocelot/Middleware/OcelotPipelineExtensions.cs b/src/Ocelot/Middleware/OcelotPipelineExtensions.cs index 01ec573fb8..16f4a5cff1 100644 --- a/src/Ocelot/Middleware/OcelotPipelineExtensions.cs +++ b/src/Ocelot/Middleware/OcelotPipelineExtensions.cs @@ -12,7 +12,7 @@ using Ocelot.LoadBalancer.Middleware; using Ocelot.Multiplexer; using Ocelot.QueryStrings.Middleware; -using Ocelot.RateLimit.Middleware; +using Ocelot.RateLimiting.Middleware; using Ocelot.Request.Middleware; using Ocelot.Requester.Middleware; using Ocelot.RequestId.Middleware; diff --git a/src/Ocelot/Multiplexer/InMemoryResponseAggregatorFactory.cs b/src/Ocelot/Multiplexer/InMemoryResponseAggregatorFactory.cs index 8983caea93..ae838e6d03 100644 --- a/src/Ocelot/Multiplexer/InMemoryResponseAggregatorFactory.cs +++ b/src/Ocelot/Multiplexer/InMemoryResponseAggregatorFactory.cs @@ -14,13 +14,6 @@ public InMemoryResponseAggregatorFactory(IDefinedAggregatorProvider provider, IR } public IResponseAggregator Get(Route route) - { - if (!string.IsNullOrEmpty(route.Aggregator)) - { - return _userDefined; - } - - return _simple; - } + => !string.IsNullOrEmpty(route.Aggregator) ? _userDefined : _simple; } } diff --git a/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs b/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs index b6fb52a373..43a98fcd3f 100644 --- a/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs +++ b/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs @@ -1,219 +1,284 @@ using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Newtonsoft.Json.Linq; using Ocelot.Configuration; +using Ocelot.Configuration.File; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Logging; using Ocelot.Middleware; +using System.Collections; +using Route = Ocelot.Configuration.Route; -namespace Ocelot.Multiplexer +namespace Ocelot.Multiplexer; + +public class MultiplexingMiddleware : OcelotMiddleware { - public class MultiplexingMiddleware : OcelotMiddleware + private readonly RequestDelegate _next; + private readonly IResponseAggregatorFactory _factory; + private const string RequestIdString = "RequestId"; + + public MultiplexingMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IResponseAggregatorFactory factory) + : base(loggerFactory.CreateLogger()) { - private readonly RequestDelegate _next; - private readonly IResponseAggregatorFactory _factory; - - public MultiplexingMiddleware(RequestDelegate next, - IOcelotLoggerFactory loggerFactory, - IResponseAggregatorFactory factory - ) - : base(loggerFactory.CreateLogger()) + _factory = factory; + _next = next; + } + + public async Task Invoke(HttpContext httpContext) + { + var downstreamRouteHolder = httpContext.Items.DownstreamRouteHolder(); + var route = downstreamRouteHolder.Route; + var downstreamRoutes = route.DownstreamRoute; + + // Case 1: if websocket request or single downstream route + if (ShouldProcessSingleRoute(httpContext, downstreamRoutes)) { - _factory = factory; - _next = next; + await ProcessSingleRouteAsync(httpContext, downstreamRoutes[0]); + return; } - public async Task Invoke(HttpContext httpContext) + // Case 2: if no downstream routes + if (downstreamRoutes.Count == 0) { - if (httpContext.WebSockets.IsWebSocketRequest) - { - //todo this is obviously stupid - httpContext.Items.UpsertDownstreamRoute(httpContext.Items.DownstreamRouteHolder().Route.DownstreamRoute[0]); - await _next.Invoke(httpContext); - return; - } + return; + } - var routeKeysConfigs = httpContext.Items.DownstreamRouteHolder().Route.DownstreamRouteConfig; - if (routeKeysConfigs == null || !routeKeysConfigs.Any()) - { - var downstreamRouteHolder = httpContext.Items.DownstreamRouteHolder(); + // Case 3: if multiple downstream routes + var routeKeysConfigs = route.DownstreamRouteConfig; + if (routeKeysConfigs == null || routeKeysConfigs.Count == 0) + { + await ProcessRoutesAsync(httpContext, route); + return; + } - var tasks = new Task[downstreamRouteHolder.Route.DownstreamRoute.Count]; + // Case 4: if multiple downstream routes with route keys + var mainResponseContext = await ProcessMainRouteAsync(httpContext, downstreamRoutes[0]); + if (mainResponseContext == null) + { + return; + } - for (var i = 0; i < downstreamRouteHolder.Route.DownstreamRoute.Count; i++) - { - var newHttpContext = Copy(httpContext); + var responsesContexts = await ProcessRoutesWithRouteKeysAsync(httpContext, downstreamRoutes, routeKeysConfigs, mainResponseContext); + if (responsesContexts.Length == 0) + { + return; + } - newHttpContext.Items - .Add("RequestId", httpContext.Items["RequestId"]); - newHttpContext.Items - .SetIInternalConfiguration(httpContext.Items.IInternalConfiguration()); - newHttpContext.Items - .UpsertTemplatePlaceholderNameAndValues(httpContext.Items.TemplatePlaceholderNameAndValues()); - newHttpContext.Items - .UpsertDownstreamRoute(downstreamRouteHolder.Route.DownstreamRoute[i]); + await MapResponsesAsync(httpContext, route, mainResponseContext, responsesContexts); + } - tasks[i] = Fire(newHttpContext, _next); - } + /// + /// Helper method to determine if only the first downstream route should be processed. + /// It is the case if the request is a websocket request or if there is only one downstream route. + /// + /// The http context. + /// The downstream routes. + /// True if only the first downstream route should be processed. + private static bool ShouldProcessSingleRoute(HttpContext context, ICollection routes) + => context.WebSockets.IsWebSocketRequest || routes.Count == 1; + + /// + /// Processing a single downstream route (no route keys). + /// In that case, no need to make copies of the http context. + /// + /// The http context. + /// The downstream route. + /// A representing the asynchronous operation. + protected virtual Task ProcessSingleRouteAsync(HttpContext context, DownstreamRoute route) + { + context.Items.UpsertDownstreamRoute(route); + return _next.Invoke(context); + } - await Task.WhenAll(tasks); + /// + /// Processing the downstream routes (no route keys). + /// + /// The main http context. + /// The route. + private async Task ProcessRoutesAsync(HttpContext context, Route route) + { + var tasks = route.DownstreamRoute + .Select(downstreamRoute => ProcessRouteAsync(context, downstreamRoute)) + .ToArray(); + var contexts = await Task.WhenAll(tasks); + await MapAsync(context, route, new(contexts)); + } - var contexts = new List(); + /// + /// When using route keys, the first route is the main route and the rest are additional routes. + /// Since we need to break if the main route response is null, we must process the main route first. + /// + /// The http context. + /// The first route, the main route. + /// The updated http context. + private async Task ProcessMainRouteAsync(HttpContext context, DownstreamRoute route) + { + context.Items.UpsertDownstreamRoute(route); + await _next.Invoke(context); + return context; + } - foreach (var task in tasks) - { - var finished = await task; - contexts.Add(finished); - } + /// + /// Processing the downstream routes with route keys except the main route that has already been processed. + /// + /// The main http context. + /// The downstream routes. + /// The route keys config. + /// The response from the main route. + /// A list of the tasks' http contexts. + protected virtual async Task ProcessRoutesWithRouteKeysAsync(HttpContext context, IEnumerable routes, IReadOnlyCollection routeKeysConfigs, HttpContext mainResponse) + { + var processing = new List>(); + var content = await mainResponse.Items.DownstreamResponse().Content.ReadAsStringAsync(); + var jObject = JToken.Parse(content); - await Map(httpContext, downstreamRouteHolder.Route, contexts); - } - else + foreach (var downstreamRoute in routes.Skip(1)) + { + var matchAdvancedAgg = routeKeysConfigs.FirstOrDefault(q => q.RouteKey == downstreamRoute.Key); + if (matchAdvancedAgg != null) { - httpContext.Items.UpsertDownstreamRoute(httpContext.Items.DownstreamRouteHolder().Route.DownstreamRoute[0]); - var mainResponse = await Fire(httpContext, _next); - - if (httpContext.Items.DownstreamRouteHolder().Route.DownstreamRoute.Count == 1) - { - MapNotAggregate(httpContext, new List { mainResponse }); - return; - } - - var tasks = new List>(); - - if (mainResponse.Items.DownstreamResponse() == null) - { - return; - } - - var content = await mainResponse.Items.DownstreamResponse().Content.ReadAsStringAsync(); - - var jObject = Newtonsoft.Json.Linq.JToken.Parse(content); - - for (var i = 1; i < httpContext.Items.DownstreamRouteHolder().Route.DownstreamRoute.Count; i++) - { - var templatePlaceholderNameAndValues = httpContext.Items.TemplatePlaceholderNameAndValues(); - - var downstreamRoute = httpContext.Items.DownstreamRouteHolder().Route.DownstreamRoute[i]; - - var matchAdvancedAgg = routeKeysConfigs - .FirstOrDefault(q => q.RouteKey == downstreamRoute.Key); - - if (matchAdvancedAgg != null) - { - var values = jObject.SelectTokens(matchAdvancedAgg.JsonPath).Select(s => s.ToString()).Distinct(); - - foreach (var value in values) - { - var newHttpContext = Copy(httpContext); - - var tPnv = httpContext.Items.TemplatePlaceholderNameAndValues(); - tPnv.Add(new PlaceholderNameAndValue('{' + matchAdvancedAgg.Parameter + '}', value)); - - newHttpContext.Items - .Add("RequestId", httpContext.Items["RequestId"]); - - newHttpContext.Items - .SetIInternalConfiguration(httpContext.Items.IInternalConfiguration()); - - newHttpContext.Items - .UpsertTemplatePlaceholderNameAndValues(tPnv); - - newHttpContext.Items - .UpsertDownstreamRoute(downstreamRoute); - - tasks.Add(Fire(newHttpContext, _next)); - } - } - else - { - var newHttpContext = Copy(httpContext); - - newHttpContext.Items - .Add("RequestId", httpContext.Items["RequestId"]); - - newHttpContext.Items - .SetIInternalConfiguration(httpContext.Items.IInternalConfiguration()); - - newHttpContext.Items - .UpsertTemplatePlaceholderNameAndValues(templatePlaceholderNameAndValues); - - newHttpContext.Items - .UpsertDownstreamRoute(downstreamRoute); - - tasks.Add(Fire(newHttpContext, _next)); - } - } + processing.AddRange(ProcessRouteWithComplexAggregation(matchAdvancedAgg, jObject, context, downstreamRoute)); + continue; + } - await Task.WhenAll(tasks); + processing.Add(ProcessRouteAsync(context, downstreamRoute)); + } - var contexts = new List { mainResponse }; + return await Task.WhenAll(processing); + } - foreach (var task in tasks) - { - var finished = await task; - contexts.Add(finished); - } + /// + /// Mapping responses. + /// + private Task MapResponsesAsync(HttpContext context, Route route, HttpContext mainResponseContext, IEnumerable responsesContexts) + { + var contexts = new List { mainResponseContext }; + contexts.AddRange(responsesContexts); + return MapAsync(context, route, contexts); + } - await Map(httpContext, httpContext.Items.DownstreamRouteHolder().Route, contexts); - } + /// + /// Processing a route with aggregation. + /// + private IEnumerable> ProcessRouteWithComplexAggregation(AggregateRouteConfig matchAdvancedAgg, + JToken jObject, HttpContext httpContext, DownstreamRoute downstreamRoute) + { + var processing = new List>(); + var values = jObject.SelectTokens(matchAdvancedAgg.JsonPath).Select(s => s.ToString()).Distinct(); + foreach (var value in values) + { + var tPnv = httpContext.Items.TemplatePlaceholderNameAndValues(); + tPnv.Add(new PlaceholderNameAndValue('{' + matchAdvancedAgg.Parameter + '}', value)); + processing.Add(ProcessRouteAsync(httpContext, downstreamRoute, tPnv)); } - private static HttpContext Copy(HttpContext source) - { - var target = new DefaultHttpContext(); + return processing; + } - foreach (var header in source.Request.Headers) - { - target.Request.Headers.TryAdd(header.Key, header.Value); - } + /// + /// Process a downstream route asynchronously. + /// + /// The cloned Http context. + private async Task ProcessRouteAsync(HttpContext sourceContext, DownstreamRoute route, List placeholders = null) + { + var newHttpContext = await CreateThreadContextAsync(sourceContext); + CopyItemsToNewContext(newHttpContext, sourceContext, placeholders); + newHttpContext.Items.UpsertDownstreamRoute(route); - target.Request.Body = source.Request.Body; - target.Request.ContentLength = source.Request.ContentLength; - target.Request.ContentType = source.Request.ContentType; - target.Request.Host = source.Request.Host; - target.Request.Method = source.Request.Method; - target.Request.Path = source.Request.Path; - target.Request.PathBase = source.Request.PathBase; - target.Request.Protocol = source.Request.Protocol; - target.Request.Query = source.Request.Query; - target.Request.QueryString = source.Request.QueryString; - target.Request.Scheme = source.Request.Scheme; - target.Request.IsHttps = source.Request.IsHttps; - target.Request.RouteValues = source.Request.RouteValues; - target.Connection.RemoteIpAddress = source.Connection.RemoteIpAddress; - target.RequestServices = source.RequestServices; - target.RequestAborted = source.RequestAborted; - return target; - } + await _next.Invoke(newHttpContext); + return newHttpContext; + } + + /// + /// Copying some needed parameters to the Http context items. + /// + private static void CopyItemsToNewContext(HttpContext target, HttpContext source, List placeholders = null) + { + target.Items.Add(RequestIdString, source.Items[RequestIdString]); + target.Items.SetIInternalConfiguration(source.Items.IInternalConfiguration()); + target.Items.UpsertTemplatePlaceholderNameAndValues(placeholders ?? + source.Items.TemplatePlaceholderNameAndValues()); + } - private async Task Map(HttpContext httpContext, Route route, List contexts) + /// + /// Creates a new HttpContext based on the source. + /// + /// The base http context. + /// The cloned context. + protected virtual async Task CreateThreadContextAsync(HttpContext source) + { + var from = source.Request; + var bodyStream = await CloneRequestBodyAsync(from, source.RequestAborted); + var target = new DefaultHttpContext { - if (route.DownstreamRoute.Count > 1) + Request = { - var aggregator = _factory.Get(route); - await aggregator.Aggregate(route, httpContext, contexts); - } - else + Body = bodyStream, + ContentLength = from.ContentLength, + ContentType = from.ContentType, + Host = from.Host, + Method = from.Method, + Path = from.Path, + PathBase = from.PathBase, + Protocol = from.Protocol, + QueryString = from.QueryString, + Scheme = from.Scheme, + IsHttps = from.IsHttps, + Query = new QueryCollection(new Dictionary(from.Query)), + RouteValues = new(from.RouteValues), + }, + Connection = { - MapNotAggregate(httpContext, contexts); - } - } - - private static void MapNotAggregate(HttpContext httpContext, List downstreamContexts) + RemoteIpAddress = source.Connection.RemoteIpAddress, + }, + RequestServices = source.RequestServices, + RequestAborted = source.RequestAborted, + User = source.User, + }; + foreach (var header in from.Headers) { - //assume at least one..if this errors then it will be caught by global exception handler - var finished = downstreamContexts.First(); - - httpContext.Items.UpsertErrors(finished.Items.Errors()); - - httpContext.Items.UpsertDownstreamRequest(finished.Items.DownstreamRequest()); - - httpContext.Items.UpsertDownstreamResponse(finished.Items.DownstreamResponse()); - } + target.Request.Headers[header.Key] = header.Value.ToArray(); + } + + // Once the downstream request is completed and the downstream response has been read, the downstream response object can dispose of the body's Stream object + target.Response.RegisterForDisposeAsync(bodyStream); // manage Stream lifetime by HttpResponse object + return target; + } - private static async Task Fire(HttpContext httpContext, RequestDelegate next) + protected virtual Task MapAsync(HttpContext httpContext, Route route, List contexts) + { + if (route.DownstreamRoute.Count == 1) { - await next.Invoke(httpContext); - return httpContext; + return Task.CompletedTask; } - } + + var aggregator = _factory.Get(route); + return aggregator.Aggregate(route, httpContext, contexts); + } + + protected virtual async Task CloneRequestBodyAsync(HttpRequest request, CancellationToken aborted) + { + request.EnableBuffering(); + if (request.Body.Position != 0) + { + Logger.LogWarning("Ocelot does not support body copy without stream in initial position 0"); + return request.Body; + } + + var targetBuffer = new MemoryStream(); + if (request.ContentLength is not null) + { + await request.Body.CopyToAsync(targetBuffer, (int)request.ContentLength, aborted); + targetBuffer.Position = 0; + request.Body.Position = 0; + } + else + { + Logger.LogWarning("Aggregation does not support body copy without Content-Length header!"); + } + + return targetBuffer; + } } diff --git a/src/Ocelot/Multiplexer/ServiceLocatorDefinedAggregatorProvider.cs b/src/Ocelot/Multiplexer/ServiceLocatorDefinedAggregatorProvider.cs index bbb2457ad5..d0841eeba7 100644 --- a/src/Ocelot/Multiplexer/ServiceLocatorDefinedAggregatorProvider.cs +++ b/src/Ocelot/Multiplexer/ServiceLocatorDefinedAggregatorProvider.cs @@ -15,9 +15,9 @@ public ServiceLocatorDefinedAggregatorProvider(IServiceProvider services) public Response Get(Route route) { - if (_aggregators.ContainsKey(route.Aggregator)) + if (_aggregators.TryGetValue(route.Aggregator, out var aggregator)) { - return new OkResponse(_aggregators[route.Aggregator]); + return new OkResponse(aggregator); } return new ErrorResponse(new CouldNotFindAggregatorError(route.Aggregator)); diff --git a/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs b/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs deleted file mode 100644 index 14ebc05946..0000000000 --- a/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration; - -namespace Ocelot.RateLimit -{ - public class ClientRateLimitProcessor - { - private readonly RateLimitCore _core; - - public ClientRateLimitProcessor(IRateLimitCounterHandler counterHandler) - { - _core = new RateLimitCore(counterHandler); - } - - public RateLimitCounter ProcessRequest(ClientRequestIdentity requestIdentity, RateLimitOptions option) - { - return _core.ProcessRequest(requestIdentity, option); - } - - public int RetryAfterFrom(DateTime timestamp, RateLimitRule rule) - { - return _core.RetryAfterFrom(timestamp, rule); - } - - public RateLimitHeaders GetRateLimitHeaders(HttpContext context, ClientRequestIdentity requestIdentity, RateLimitOptions option) - { - return _core.GetRateLimitHeaders(context, requestIdentity, option); - } - - public TimeSpan ConvertToTimeSpan(string timeSpan) - { - return _core.ConvertToTimeSpan(timeSpan); - } - } -} diff --git a/src/Ocelot/RateLimit/ClientRequestIdentity.cs b/src/Ocelot/RateLimit/ClientRequestIdentity.cs deleted file mode 100644 index b67b7c5a97..0000000000 --- a/src/Ocelot/RateLimit/ClientRequestIdentity.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Ocelot.RateLimit -{ - public class ClientRequestIdentity - { - public ClientRequestIdentity(string clientId, string path, string httpverb) - { - ClientId = clientId; - Path = path; - HttpVerb = httpverb; - } - - public string ClientId { get; } - - public string Path { get; } - - public string HttpVerb { get; } - } -} diff --git a/src/Ocelot/RateLimit/DistributedCacheRateLimitCounterHanlder.cs b/src/Ocelot/RateLimit/DistributedCacheRateLimitCounterHanlder.cs deleted file mode 100644 index e286e3161a..0000000000 --- a/src/Ocelot/RateLimit/DistributedCacheRateLimitCounterHanlder.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.Extensions.Caching.Distributed; -using Newtonsoft.Json; - -namespace Ocelot.RateLimit -{ - public class DistributedCacheRateLimitCounterHanlder : IRateLimitCounterHandler - { - private readonly IDistributedCache _memoryCache; - - public DistributedCacheRateLimitCounterHanlder(IDistributedCache memoryCache) - { - _memoryCache = memoryCache; - } - - public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime) - { - _memoryCache.SetString(id, JsonConvert.SerializeObject(counter), new DistributedCacheEntryOptions().SetAbsoluteExpiration(expirationTime)); - } - - public bool Exists(string id) - { - var stored = _memoryCache.GetString(id); - return !string.IsNullOrEmpty(stored); - } - - public RateLimitCounter? Get(string id) - { - var stored = _memoryCache.GetString(id); - if (!string.IsNullOrEmpty(stored)) - { - return JsonConvert.DeserializeObject(stored); - } - - return null; - } - - public void Remove(string id) - { - _memoryCache.Remove(id); - } - } -} diff --git a/src/Ocelot/RateLimit/IRateLimitCounterHandler.cs b/src/Ocelot/RateLimit/IRateLimitCounterHandler.cs deleted file mode 100644 index c17d04f7c0..0000000000 --- a/src/Ocelot/RateLimit/IRateLimitCounterHandler.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Ocelot.RateLimit -{ - public interface IRateLimitCounterHandler - { - bool Exists(string id); - - RateLimitCounter? Get(string id); - - void Remove(string id); - - void Set(string id, RateLimitCounter counter, TimeSpan expirationTime); - } -} diff --git a/src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs b/src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs deleted file mode 100644 index 1a030d511b..0000000000 --- a/src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.Extensions.Caching.Memory; - -namespace Ocelot.RateLimit -{ - public class MemoryCacheRateLimitCounterHandler : IRateLimitCounterHandler - { - private readonly IMemoryCache _memoryCache; - - public MemoryCacheRateLimitCounterHandler(IMemoryCache memoryCache) - { - _memoryCache = memoryCache; - } - - public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime) - { - _memoryCache.Set(id, counter, new MemoryCacheEntryOptions().SetAbsoluteExpiration(expirationTime)); - } - - public bool Exists(string id) => _memoryCache.TryGetValue(id, out RateLimitCounter counter); - - public RateLimitCounter? Get(string id) => _memoryCache.TryGetValue(id, out RateLimitCounter counter) ? counter : null; - - public void Remove(string id) - { - _memoryCache.Remove(id); - } - } -} diff --git a/src/Ocelot/RateLimit/Middleware/RateLimitMiddlewareExtensions.cs b/src/Ocelot/RateLimit/Middleware/RateLimitMiddlewareExtensions.cs deleted file mode 100644 index 91609c67f3..0000000000 --- a/src/Ocelot/RateLimit/Middleware/RateLimitMiddlewareExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.AspNetCore.Builder; - -namespace Ocelot.RateLimit.Middleware -{ - public static class RateLimitMiddlewareExtensions - { - public static IApplicationBuilder UseRateLimiting(this IApplicationBuilder builder) - { - return builder.UseMiddleware(); - } - } -} diff --git a/src/Ocelot/RateLimit/RateLimitCore.cs b/src/Ocelot/RateLimit/RateLimitCore.cs deleted file mode 100644 index dddf8a7726..0000000000 --- a/src/Ocelot/RateLimit/RateLimitCore.cs +++ /dev/null @@ -1,147 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration; -using System.Globalization; -using System.Security.Cryptography; - -namespace Ocelot.RateLimit -{ - public class RateLimitCore - { - private readonly IRateLimitCounterHandler _counterHandler; - private static readonly object ProcessLocker = new(); - - public RateLimitCore(IRateLimitCounterHandler counterStore) - { - _counterHandler = counterStore; - } - - public RateLimitCounter ProcessRequest(ClientRequestIdentity requestIdentity, RateLimitOptions option) - { - var counter = new RateLimitCounter(DateTime.UtcNow, 1); - var rule = option.RateLimitRule; - - var counterId = ComputeCounterKey(requestIdentity, option); - - // serial reads and writes - lock (ProcessLocker) - { - var entry = _counterHandler.Get(counterId); - if (entry.HasValue) - { - // entry has not expired - if (entry.Value.Timestamp + TimeSpan.FromSeconds(rule.PeriodTimespan) >= DateTime.UtcNow) - { - // increment request count - var totalRequests = entry.Value.TotalRequests + 1; - - // deep copy - counter = new RateLimitCounter(entry.Value.Timestamp, totalRequests); - } - } - } - - if (counter.TotalRequests > rule.Limit) - { - var retryAfter = RetryAfterFrom(counter.Timestamp, rule); - if (retryAfter > 0) - { - var expirationTime = TimeSpan.FromSeconds(rule.PeriodTimespan); - _counterHandler.Set(counterId, counter, expirationTime); - } - else - { - _counterHandler.Remove(counterId); - } - } - else - { - var expirationTime = ConvertToTimeSpan(rule.Period); - _counterHandler.Set(counterId, counter, expirationTime); - } - - return counter; - } - - public void SaveRateLimitCounter(ClientRequestIdentity requestIdentity, RateLimitOptions option, RateLimitCounter counter, TimeSpan expirationTime) - { - var counterId = ComputeCounterKey(requestIdentity, option); - var rule = option.RateLimitRule; - - // stores: id (string) - timestamp (datetime) - total_requests (long) - _counterHandler.Set(counterId, counter, expirationTime); - } - - public RateLimitHeaders GetRateLimitHeaders(HttpContext context, ClientRequestIdentity requestIdentity, RateLimitOptions option) - { - var rule = option.RateLimitRule; - RateLimitHeaders headers; - var counterId = ComputeCounterKey(requestIdentity, option); - var entry = _counterHandler.Get(counterId); - if (entry.HasValue) - { - headers = new RateLimitHeaders(context, rule.Period, - (rule.Limit - entry.Value.TotalRequests).ToString(), - (entry.Value.Timestamp + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo) - ); - } - else - { - headers = new RateLimitHeaders(context, - rule.Period, - rule.Limit.ToString(), - (DateTime.UtcNow + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo)); - } - - return headers; - } - - public string ComputeCounterKey(ClientRequestIdentity requestIdentity, RateLimitOptions option) - { - var key = $"{option.RateLimitCounterPrefix}_{requestIdentity.ClientId}_{option.RateLimitRule.Period}_{requestIdentity.HttpVerb}_{requestIdentity.Path}"; - - var idBytes = Encoding.UTF8.GetBytes(key); - - byte[] hashBytes; - - using (var algorithm = SHA1.Create()) - { - hashBytes = algorithm.ComputeHash(idBytes); - } - - return BitConverter.ToString(hashBytes).Replace("-", string.Empty); - } - - public int RetryAfterFrom(DateTime timestamp, RateLimitRule rule) - { - var secondsPast = Convert.ToInt32((DateTime.UtcNow - timestamp).TotalSeconds); - var retryAfter = Convert.ToInt32(TimeSpan.FromSeconds(rule.PeriodTimespan).TotalSeconds); - retryAfter = retryAfter > 1 ? retryAfter - secondsPast : 1; - return retryAfter; - } - - public TimeSpan ConvertToTimeSpan(string timeSpan) - { - var l = timeSpan.Length - 1; - var value = timeSpan.Substring(0, l); - var type = timeSpan.Substring(l, 1); - - switch (type) - { - case "d": - return TimeSpan.FromDays(double.Parse(value)); - - case "h": - return TimeSpan.FromHours(double.Parse(value)); - - case "m": - return TimeSpan.FromMinutes(double.Parse(value)); - - case "s": - return TimeSpan.FromSeconds(double.Parse(value)); - - default: - throw new FormatException($"{timeSpan} can't be converted to TimeSpan, unknown type {type}"); - } - } - } -} diff --git a/src/Ocelot/RateLimit/RateLimitCounter.cs b/src/Ocelot/RateLimit/RateLimitCounter.cs deleted file mode 100644 index 4e869d4402..0000000000 --- a/src/Ocelot/RateLimit/RateLimitCounter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Newtonsoft.Json; - -namespace Ocelot.RateLimit -{ - /// - /// Stores the initial access time and the numbers of calls made from that point. - /// - public struct RateLimitCounter - { - [JsonConstructor] - public RateLimitCounter(DateTime timestamp, long totalRequests) - { - Timestamp = timestamp; - TotalRequests = totalRequests; - } - - public DateTime Timestamp { get; } - - public long TotalRequests { get; } - } -} diff --git a/src/Ocelot/RateLimit/RateLimitHeaders.cs b/src/Ocelot/RateLimit/RateLimitHeaders.cs deleted file mode 100644 index 67d7596ced..0000000000 --- a/src/Ocelot/RateLimit/RateLimitHeaders.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace Ocelot.RateLimit -{ - public class RateLimitHeaders - { - public RateLimitHeaders(HttpContext context, string limit, string remaining, string reset) - { - Context = context; - Limit = limit; - Remaining = remaining; - Reset = reset; - } - - public HttpContext Context { get; } - - public string Limit { get; } - - public string Remaining { get; } - - public string Reset { get; } - } -} diff --git a/src/Ocelot/RateLimiting/ClientRequestIdentity.cs b/src/Ocelot/RateLimiting/ClientRequestIdentity.cs new file mode 100644 index 0000000000..b73fcbbbb3 --- /dev/null +++ b/src/Ocelot/RateLimiting/ClientRequestIdentity.cs @@ -0,0 +1,15 @@ +namespace Ocelot.RateLimiting; + +public class ClientRequestIdentity +{ + public ClientRequestIdentity(string clientId, string path, string httpverb) + { + ClientId = clientId; + Path = path; + HttpVerb = httpverb; + } + + public string ClientId { get; } + public string Path { get; } + public string HttpVerb { get; } +} diff --git a/src/Ocelot/RateLimiting/DistributedCacheRateLimitStorage.cs b/src/Ocelot/RateLimiting/DistributedCacheRateLimitStorage.cs new file mode 100644 index 0000000000..b7fb79de3d --- /dev/null +++ b/src/Ocelot/RateLimiting/DistributedCacheRateLimitStorage.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.Caching.Distributed; +using Newtonsoft.Json; + +namespace Ocelot.RateLimiting; + +/// +/// Custom storage based on a distributed cache of a remote/local services. +/// +/// +/// See the interface docs for more details. +/// +public class DistributedCacheRateLimitStorage : IRateLimitStorage +{ + private readonly IDistributedCache _memoryCache; + + public DistributedCacheRateLimitStorage(IDistributedCache memoryCache) => _memoryCache = memoryCache; + + public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime) + => _memoryCache.SetString(id, JsonConvert.SerializeObject(counter), new DistributedCacheEntryOptions().SetAbsoluteExpiration(expirationTime)); + + public bool Exists(string id) => !string.IsNullOrEmpty(_memoryCache.GetString(id)); + + public RateLimitCounter? Get(string id) + { + var stored = _memoryCache.GetString(id); + return !string.IsNullOrEmpty(stored) + ? JsonConvert.DeserializeObject(stored) + : null; + } + + public void Remove(string id) => _memoryCache.Remove(id); +} diff --git a/src/Ocelot/RateLimiting/IRateLimitStorage.cs b/src/Ocelot/RateLimiting/IRateLimitStorage.cs new file mode 100644 index 0000000000..1044998b12 --- /dev/null +++ b/src/Ocelot/RateLimiting/IRateLimitStorage.cs @@ -0,0 +1,16 @@ +namespace Ocelot.RateLimiting; + +/// +/// Defines a storage for keeping of rate limiting data. +/// +/// Concrete classes should be based on solutions with excellent performance, such as in-memory solutions. +public interface IRateLimitStorage +{ + bool Exists(string id); + + RateLimitCounter? Get(string id); + + void Remove(string id); + + void Set(string id, RateLimitCounter counter, TimeSpan expirationTime); +} diff --git a/src/Ocelot/RateLimiting/IRateLimiting.cs b/src/Ocelot/RateLimiting/IRateLimiting.cs new file mode 100644 index 0000000000..684d2f70e9 --- /dev/null +++ b/src/Ocelot/RateLimiting/IRateLimiting.cs @@ -0,0 +1,59 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; + +namespace Ocelot.RateLimiting; + +/// +/// Defines basic Rate Limiting functionality. +/// +public interface IRateLimiting +{ + /// Retrieves the key for the attached storage. + /// See the interface. + /// The current representation of the request. + /// The options of rate limiting. + /// A value of the key. + string GetStorageKey(ClientRequestIdentity identity, RateLimitOptions options); + + /// + /// Gets required information to create wanted headers in upper contexts (middleware, etc). + /// + /// The current context. + /// The current representation of the request. + /// The options of rate limiting. + /// A value. + RateLimitHeaders GetHeaders(HttpContext context, ClientRequestIdentity identity, RateLimitOptions options); + + /// + /// Main entry point to process the current request and apply the limiting rule. + /// + /// Warning! The method performs the storage operations which should be thread safe. + /// The representation of current request. + /// The current rate limiting options. + /// A value. + RateLimitCounter ProcessRequest(ClientRequestIdentity identity, RateLimitOptions options); + + /// + /// Counts requests based on the current counter state and taking into account the limiting rule. + /// + /// Old counter with starting moment inside. + /// The limiting rule. + /// A value. + RateLimitCounter Count(RateLimitCounter? entry, RateLimitRule rule); + + /// + /// Gets the seconds to wait for the next retry by starting moment and the rule. + /// + /// The method must be called after the counting by the method is completed; otherwise it doesn't make sense. + /// The counter with starting moment inside. + /// The limiting rule. + /// A value in seconds. + double RetryAfter(RateLimitCounter counter, RateLimitRule rule); + + /// + /// Converts to time span from a string, such as "1s", "1m", "1h", "1d". + /// + /// The string value with dimentions: '1s', '1m', '1h', '1d'. + /// A value. + TimeSpan ToTimespan(string timespan); +} diff --git a/src/Ocelot/RateLimiting/MemoryCacheRateLimitStorage.cs b/src/Ocelot/RateLimiting/MemoryCacheRateLimitStorage.cs new file mode 100644 index 0000000000..7451dac97e --- /dev/null +++ b/src/Ocelot/RateLimiting/MemoryCacheRateLimitStorage.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Caching.Memory; + +namespace Ocelot.RateLimiting; + +/// +/// Default storage based on the memory cache of the local web server instance. +/// +/// +/// See the interface docs for more details. +/// +public class MemoryCacheRateLimitStorage : IRateLimitStorage +{ + private readonly IMemoryCache _memoryCache; + + public MemoryCacheRateLimitStorage(IMemoryCache memoryCache) => _memoryCache = memoryCache; + + public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime) + => _memoryCache.Set(id, counter, new MemoryCacheEntryOptions().SetAbsoluteExpiration(expirationTime)); + + public bool Exists(string id) => _memoryCache.TryGetValue(id, out RateLimitCounter counter); + + public RateLimitCounter? Get(string id) => _memoryCache.TryGetValue(id, out RateLimitCounter counter) ? counter : null; + + public void Remove(string id) => _memoryCache.Remove(id); +} diff --git a/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs b/src/Ocelot/RateLimiting/Middleware/RateLimitingMiddleware.cs similarity index 83% rename from src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs rename to src/Ocelot/RateLimiting/Middleware/RateLimitingMiddleware.cs index 47571046fb..b407733ae6 100644 --- a/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs +++ b/src/Ocelot/RateLimiting/Middleware/RateLimitingMiddleware.cs @@ -2,21 +2,23 @@ using Ocelot.Configuration; using Ocelot.Logging; using Ocelot.Middleware; +using System.Globalization; -namespace Ocelot.RateLimit.Middleware +namespace Ocelot.RateLimiting.Middleware { - public class ClientRateLimitMiddleware : OcelotMiddleware + public class RateLimitingMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; - private readonly ClientRateLimitProcessor _processor; + private readonly IRateLimiting _limiter; - public ClientRateLimitMiddleware(RequestDelegate next, - IOcelotLoggerFactory loggerFactory, - IRateLimitCounterHandler counterHandler) - : base(loggerFactory.CreateLogger()) + public RateLimitingMiddleware( + RequestDelegate next, + IOcelotLoggerFactory factory, + IRateLimiting limiter) + : base(factory.CreateLogger()) { _next = next; - _processor = new ClientRateLimitProcessor(counterHandler); + _limiter = limiter; } public async Task Invoke(HttpContext httpContext) @@ -48,26 +50,20 @@ public async Task Invoke(HttpContext httpContext) if (rule.Limit > 0) { // increment counter - var counter = _processor.ProcessRequest(identity, options); + var counter = _limiter.ProcessRequest(identity, options); // check if limit is reached if (counter.TotalRequests > rule.Limit) { - //compute retry after value - var retryAfter = _processor.RetryAfterFrom(counter.Timestamp, rule); - - // log blocked request - LogBlockedRequest(httpContext, identity, counter, rule, downstreamRoute); - - var retrystring = retryAfter.ToString(System.Globalization.CultureInfo.InvariantCulture); + var retryAfter = _limiter.RetryAfter(counter, rule); // compute retry after value based on counter state + LogBlockedRequest(httpContext, identity, counter, rule, downstreamRoute); // log blocked request virtually // break execution - var ds = ReturnQuotaExceededResponse(httpContext, options, retrystring); + var ds = ReturnQuotaExceededResponse(httpContext, options, retryAfter.ToString(CultureInfo.InvariantCulture)); httpContext.Items.UpsertDownstreamResponse(ds); // Set Error httpContext.Items.SetError(new QuotaExceededError(GetResponseMessage(options), options.HttpStatusCode)); - return; } } @@ -75,7 +71,7 @@ public async Task Invoke(HttpContext httpContext) //set X-Rate-Limit headers for the longest period if (!options.DisableRateLimitHeaders) { - var headers = _processor.GetRateLimitHeaders(httpContext, identity, options); + var headers = _limiter.GetHeaders(httpContext, identity, options); httpContext.Response.OnStarting(SetRateLimitHeaders, state: headers); } @@ -123,7 +119,7 @@ public virtual DownstreamResponse ReturnQuotaExceededResponse(HttpContext httpCo if (!option.DisableRateLimitHeaders) { - http.Headers.TryAddWithoutValidation("Retry-After", retryAfter); + http.Headers.TryAddWithoutValidation("Retry-After", retryAfter); // in seconds, not date string } return new DownstreamResponse(http); diff --git a/src/Ocelot/RateLimiting/Middleware/RateLimitingMiddlewareExtensions.cs b/src/Ocelot/RateLimiting/Middleware/RateLimitingMiddlewareExtensions.cs new file mode 100644 index 0000000000..68268cb406 --- /dev/null +++ b/src/Ocelot/RateLimiting/Middleware/RateLimitingMiddlewareExtensions.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.RateLimiting.Middleware; + +public static class RateLimitingMiddlewareExtensions +{ + public static IApplicationBuilder UseRateLimiting(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } +} diff --git a/src/Ocelot/RateLimit/QuotaExceededError.cs b/src/Ocelot/RateLimiting/QuotaExceededError.cs similarity index 89% rename from src/Ocelot/RateLimit/QuotaExceededError.cs rename to src/Ocelot/RateLimiting/QuotaExceededError.cs index 9c98dc5a67..a46cb4c785 100644 --- a/src/Ocelot/RateLimit/QuotaExceededError.cs +++ b/src/Ocelot/RateLimiting/QuotaExceededError.cs @@ -1,6 +1,6 @@ using Ocelot.Errors; -namespace Ocelot.RateLimit +namespace Ocelot.RateLimiting { public class QuotaExceededError : Error { diff --git a/src/Ocelot/RateLimiting/RateLimitCounter.cs b/src/Ocelot/RateLimiting/RateLimitCounter.cs new file mode 100644 index 0000000000..2507a0433a --- /dev/null +++ b/src/Ocelot/RateLimiting/RateLimitCounter.cs @@ -0,0 +1,29 @@ +using Newtonsoft.Json; + +namespace Ocelot.RateLimiting; + +/// +/// Stores the initial access time and the numbers of calls made from that point. +/// +public struct RateLimitCounter +{ + [JsonConstructor] + public RateLimitCounter(DateTime startedAt, DateTime? exceededAt, long totalRequests) + { + StartedAt = startedAt; + ExceededAt = exceededAt; + TotalRequests = totalRequests; + } + + /// The moment when the counting was started. + /// A value of the moment. + public DateTime StartedAt { get; } + + /// The moment when the limit was exceeded. + /// A value of the moment. + public DateTime? ExceededAt { get; } + + /// Total number of requests counted. + /// A value of total number. + public long TotalRequests { get; set; } +} diff --git a/src/Ocelot/RateLimiting/RateLimitHeaders.cs b/src/Ocelot/RateLimiting/RateLimitHeaders.cs new file mode 100644 index 0000000000..860e0d6bbf --- /dev/null +++ b/src/Ocelot/RateLimiting/RateLimitHeaders.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Http; + +namespace Ocelot.RateLimiting; + +public class RateLimitHeaders +{ + public RateLimitHeaders(HttpContext context, string limit, string remaining, string reset) + { + Context = context; + Limit = limit; + Remaining = remaining; + Reset = reset; + } + + public HttpContext Context { get; } + public string Limit { get; } + public string Remaining { get; } + public string Reset { get; } +} diff --git a/src/Ocelot/RateLimiting/RateLimiting.cs b/src/Ocelot/RateLimiting/RateLimiting.cs new file mode 100644 index 0000000000..9edf4a310d --- /dev/null +++ b/src/Ocelot/RateLimiting/RateLimiting.cs @@ -0,0 +1,194 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; +using System.Globalization; +using System.Security.Cryptography; + +namespace Ocelot.RateLimiting; + +public class RateLimiting : IRateLimiting +{ + private readonly IRateLimitStorage _storage; + private static readonly object ProcessLocker = new(); + + public RateLimiting(IRateLimitStorage storage) + { + _storage = storage; + } + + /// + /// Main entry point to process the current request and apply the limiting rule. + /// + /// Warning! The method performs the storage operations which MUST BE thread safe. + /// The representation of current request. + /// The current rate limiting options. + /// A value. + public virtual RateLimitCounter ProcessRequest(ClientRequestIdentity identity, RateLimitOptions options) + { + RateLimitCounter counter; + var rule = options.RateLimitRule; + var counterId = GetStorageKey(identity, options); + + // Serial reads/writes from/to the storage which must be thread safe + lock (ProcessLocker) + { + var entry = _storage.Get(counterId); + counter = Count(entry, rule); + var expiration = ToTimespan(rule.Period); // default expiration is set for the Period value + if (counter.TotalRequests > rule.Limit) + { + var retryAfter = RetryAfter(counter, rule); // the calculation depends on the counter returned from CountRequests + if (retryAfter > 0) + { + // Rate Limit exceeded, ban period is active + expiration = TimeSpan.FromSeconds(rule.PeriodTimespan); // current state should expire in the storage after ban period + } + else + { + // Ban period elapsed, start counting + _storage.Remove(counterId); // the store can delete the element on its own using an expiration mechanism, but let's force the element to be deleted + counter = new RateLimitCounter(DateTime.UtcNow, null, 1); + } + } + + _storage.Set(counterId, counter, expiration); + } + + return counter; + } + + /// + /// Counts requests based on the current counter state and taking into account the limiting rule. + /// + /// Old counter with starting moment inside. + /// The limiting rule. + /// A value. + public virtual RateLimitCounter Count(RateLimitCounter? entry, RateLimitRule rule) + { + var now = DateTime.UtcNow; + if (!entry.HasValue) // no entry, start counting + { + return new RateLimitCounter(now, null, 1); // current request is the 1st one + } + + var counter = entry.Value; + var total = counter.TotalRequests + 1; // increment request count + var startedAt = counter.StartedAt; + if (startedAt + ToTimespan(rule.Period) >= now) // counting Period is active + { + var exceededAt = total >= rule.Limit && !counter.ExceededAt.HasValue // current request number equals to the limit + ? now // the exceeding moment is now, the next request will fail but the current one doesn't + : counter.ExceededAt; + return new RateLimitCounter(startedAt, exceededAt, total); // deep copy + } + + var wasExceededAt = counter.ExceededAt; + return wasExceededAt + TimeSpan.FromSeconds(rule.PeriodTimespan) >= now // ban PeriodTimespan is active + ? new RateLimitCounter(startedAt, wasExceededAt, total) // still count + : new RateLimitCounter(now, null, 1); // Ban PeriodTimespan elapsed, start counting NOW! + } + + public virtual RateLimitHeaders GetHeaders(HttpContext context, ClientRequestIdentity identity, RateLimitOptions options) + { + RateLimitHeaders headers; + RateLimitCounter? entry; + lock (ProcessLocker) + { + var counterId = GetStorageKey(identity, options); + entry = _storage.Get(counterId); + } + + var rule = options.RateLimitRule; + if (entry.HasValue) + { + headers = new RateLimitHeaders(context, + limit: rule.Period, + remaining: (rule.Limit - entry.Value.TotalRequests).ToString(), + reset: (entry.Value.StartedAt + ToTimespan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo)); + } + else + { + headers = new RateLimitHeaders(context, + limit: rule.Period, // TODO Double check + remaining: rule.Limit.ToString(), // TODO Double check + reset: (DateTime.UtcNow + ToTimespan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo)); + } + + return headers; + } + + public virtual string GetStorageKey(ClientRequestIdentity identity, RateLimitOptions options) + { + var key = $"{options.RateLimitCounterPrefix}_{identity.ClientId}_{options.RateLimitRule.Period}_{identity.HttpVerb}_{identity.Path}"; + var idBytes = Encoding.UTF8.GetBytes(key); + + byte[] hashBytes; + using (var algorithm = SHA1.Create()) + { + hashBytes = algorithm.ComputeHash(idBytes); + } + + return BitConverter.ToString(hashBytes).Replace("-", string.Empty); + } + + /// + /// Gets the seconds to wait for the next retry by starting moment and the rule. + /// + /// The method must be called after the one. + /// The counter state. + /// The current rule. + /// An value of seconds. + public virtual double RetryAfter(RateLimitCounter counter, RateLimitRule rule) + { + const double defaultSeconds = 1.0D; // one second + var periodTimespan = rule.PeriodTimespan < defaultSeconds + ? defaultSeconds // allow values which are greater or equal to 1 second + : rule.PeriodTimespan; // good value + var now = DateTime.UtcNow; + if (counter.StartedAt + ToTimespan(rule.Period) >= now) // counting Period is active + { + return counter.TotalRequests < rule.Limit + ? 0.0D // happy path, no need to retry, current request is valid + : counter.ExceededAt.HasValue + ? periodTimespan - (now - counter.ExceededAt.Value).TotalSeconds // minus seconds past + : periodTimespan; // exceeding not yet detected -> let's ban for whole period + } + + if (counter.ExceededAt.HasValue && // limit exceeding was happen + counter.ExceededAt + TimeSpan.FromSeconds(periodTimespan) >= now) // ban PeriodTimespan is active + { + var startedAt = counter.ExceededAt.Value; // ban period was started at + double secondsPast = (now - startedAt).TotalSeconds; + double retryAfter = periodTimespan - secondsPast; + return retryAfter; // it can be negative, which means the wait in PeriodTimespan seconds has ended + } + + return 0.0D; // ban period elapsed, no need to retry, current request is valid + } + + /// + /// Converts to time span from a string, such as "1s", "1m", "1h", "1d". + /// + /// The string value with dimentions: '1s', '1m', '1h', '1d'. + /// A value. + /// By default if the value dimension can't be detected. + public virtual TimeSpan ToTimespan(string timespan) + { + if (string.IsNullOrEmpty(timespan)) + { + return TimeSpan.Zero; + } + + var len = timespan.Length - 1; + var value = timespan.Substring(0, len); + var type = timespan.Substring(len, 1); + + return type switch + { + "d" => TimeSpan.FromDays(double.Parse(value)), + "h" => TimeSpan.FromHours(double.Parse(value)), + "m" => TimeSpan.FromMinutes(double.Parse(value)), + "s" => TimeSpan.FromSeconds(double.Parse(value)), + _ => throw new FormatException($"{timespan} can't be converted to TimeSpan, unknown type {type}"), + }; + } +} diff --git a/src/Ocelot/Request/Mapper/PayloadTooLargeError.cs b/src/Ocelot/Request/Mapper/PayloadTooLargeError.cs new file mode 100644 index 0000000000..ba70819824 --- /dev/null +++ b/src/Ocelot/Request/Mapper/PayloadTooLargeError.cs @@ -0,0 +1,10 @@ +using Ocelot.Errors; + +namespace Ocelot.Request.Mapper; + +public class PayloadTooLargeError : Error +{ + public PayloadTooLargeError(Exception exception) : base(exception.Message, OcelotErrorCode.PayloadTooLargeError, (int) System.Net.HttpStatusCode.RequestEntityTooLarge) + { + } +} diff --git a/src/Ocelot/Request/Mapper/RequestMapper.cs b/src/Ocelot/Request/Mapper/RequestMapper.cs index 8bdde02bb6..883c42350c 100644 --- a/src/Ocelot/Request/Mapper/RequestMapper.cs +++ b/src/Ocelot/Request/Mapper/RequestMapper.cs @@ -1,87 +1,91 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.Extensions.Primitives; -using Ocelot.Configuration; - -namespace Ocelot.Request.Mapper; - -public class RequestMapper : IRequestMapper -{ - private static readonly HashSet UnsupportedHeaders = new(StringComparer.OrdinalIgnoreCase) { "host" }; - private static readonly string[] ContentHeaders = { "Content-Length", "Content-Language", "Content-Location", "Content-Range", "Content-MD5", "Content-Disposition", "Content-Encoding" }; - - public HttpRequestMessage Map(HttpRequest request, DownstreamRoute downstreamRoute) - { - var requestMessage = new HttpRequestMessage - { - Content = MapContent(request), - Method = MapMethod(request, downstreamRoute), - RequestUri = MapUri(request), - Version = downstreamRoute.DownstreamHttpVersion, - }; - - MapHeaders(request, requestMessage); - - return requestMessage; - } - - private static HttpContent MapContent(HttpRequest request) - { - // TODO We should check if we really need to call HttpRequest.Body.Length - // But we assume that if CanSeek is true, the length is calculated without an important overhead - if (request.Body is null or { CanSeek: true, Length: <= 0 }) - { - return null; - } - - var content = new StreamHttpContent(request.HttpContext); - - AddContentHeaders(request, content); - - return content; - } - - private static void AddContentHeaders(HttpRequest request, HttpContent content) - { - if (!string.IsNullOrEmpty(request.ContentType)) - { - content.Headers - .TryAddWithoutValidation("Content-Type", new[] { request.ContentType }); - } - - // The performance might be improved by retrieving the matching headers from the request - // instead of calling request.Headers.TryGetValue for each used content header - var matchingHeaders = ContentHeaders.Where(header => request.Headers.ContainsKey(header)); - - foreach (var key in matchingHeaders) - { - if (!request.Headers.TryGetValue(key, out var value)) - { - continue; - } - - content.Headers.TryAddWithoutValidation(key, value.ToArray()); - } - } - - private static HttpMethod MapMethod(HttpRequest request, DownstreamRoute downstreamRoute) => - !string.IsNullOrEmpty(downstreamRoute?.DownstreamHttpMethod) ? - new HttpMethod(downstreamRoute.DownstreamHttpMethod) : new HttpMethod(request.Method); - - // TODO Review this method, request.GetEncodedUrl() could throw a NullReferenceException - private static Uri MapUri(HttpRequest request) => new(request.GetEncodedUrl()); - - private static void MapHeaders(HttpRequest request, HttpRequestMessage requestMessage) - { - foreach (var header in request.Headers) - { - if (IsSupportedHeader(header)) - { - requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()); - } - } - } - - private static bool IsSupportedHeader(KeyValuePair header) => - !UnsupportedHeaders.Contains(header.Key); +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Extensions.Primitives; +using Ocelot.Configuration; + +namespace Ocelot.Request.Mapper; + +public class RequestMapper : IRequestMapper +{ + private static readonly HashSet UnsupportedHeaders = new(StringComparer.OrdinalIgnoreCase) { "host", "transfer-encoding" }; + private static readonly string[] ContentHeaders = { "Content-Length", "Content-Language", "Content-Location", "Content-Range", "Content-MD5", "Content-Disposition", "Content-Encoding" }; + + public HttpRequestMessage Map(HttpRequest request, DownstreamRoute downstreamRoute) + { + var requestMessage = new HttpRequestMessage + { + Content = MapContent(request), + Method = MapMethod(request, downstreamRoute), + RequestUri = MapUri(request), + Version = downstreamRoute.DownstreamHttpVersion, + }; + + MapHeaders(request, requestMessage); + + return requestMessage; + } + + private static HttpContent MapContent(HttpRequest request) + { + HttpContent content; + + // No content if we have no body or if the request has no content according to RFC 2616 section 4.3 + if (request.Body == null + || (!request.ContentLength.HasValue && StringValues.IsNullOrEmpty(request.Headers.TransferEncoding))) + { + return null; + } + + content = request.ContentLength is 0 + ? new ByteArrayContent(Array.Empty()) + : new StreamHttpContent(request.HttpContext); + + AddContentHeaders(request, content); + + return content; + } + + private static void AddContentHeaders(HttpRequest request, HttpContent content) + { + if (!string.IsNullOrEmpty(request.ContentType)) + { + content.Headers + .TryAddWithoutValidation("Content-Type", new[] { request.ContentType }); + } + + // The performance might be improved by retrieving the matching headers from the request + // instead of calling request.Headers.TryGetValue for each used content header + var matchingHeaders = ContentHeaders.Where(header => request.Headers.ContainsKey(header)); + + foreach (var key in matchingHeaders) + { + if (!request.Headers.TryGetValue(key, out var value)) + { + continue; + } + + content.Headers.TryAddWithoutValidation(key, value.ToArray()); + } + } + + private static HttpMethod MapMethod(HttpRequest request, DownstreamRoute downstreamRoute) => + !string.IsNullOrEmpty(downstreamRoute?.DownstreamHttpMethod) ? + new HttpMethod(downstreamRoute.DownstreamHttpMethod) : new HttpMethod(request.Method); + + // TODO Review this method, request.GetEncodedUrl() could throw a NullReferenceException + private static Uri MapUri(HttpRequest request) => new(request.GetEncodedUrl()); + + private static void MapHeaders(HttpRequest request, HttpRequestMessage requestMessage) + { + foreach (var header in request.Headers) + { + if (IsSupportedHeader(header)) + { + requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()); + } + } + } + + private static bool IsSupportedHeader(KeyValuePair header) => + !UnsupportedHeaders.Contains(header.Key); } diff --git a/src/Ocelot/Request/Mapper/StreamHttpContent.cs b/src/Ocelot/Request/Mapper/StreamHttpContent.cs index 0e8294db7d..b18e1040ee 100644 --- a/src/Ocelot/Request/Mapper/StreamHttpContent.cs +++ b/src/Ocelot/Request/Mapper/StreamHttpContent.cs @@ -8,25 +8,24 @@ public class StreamHttpContent : HttpContent private const int DefaultBufferSize = 65536; public const long UnknownLength = -1; private readonly HttpContext _context; + private readonly long _contentLength; public StreamHttpContent(HttpContext context) { _context = context ?? throw new ArgumentNullException(nameof(context)); + _contentLength = context.Request.ContentLength ?? UnknownLength; } - protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context, - CancellationToken cancellationToken) - => await CopyAsync(_context.Request.Body, stream, Headers.ContentLength ?? UnknownLength, false, - cancellationToken); + protected override Task SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken) + => CopyAsync(_context.Request.Body, stream, _contentLength, false, cancellationToken); - protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) - => await CopyAsync(_context.Request.Body, stream, Headers.ContentLength ?? UnknownLength, false, - CancellationToken.None); + protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) + => CopyAsync(_context.Request.Body, stream, _contentLength, false, CancellationToken.None); protected override bool TryComputeLength(out long length) { - length = -1; - return false; + length = _contentLength; + return length >= 0; } // This is used internally by HttpContent.ReadAsStreamAsync(...) diff --git a/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs b/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs index 6440e3d461..f7da439606 100644 --- a/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs +++ b/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs @@ -35,7 +35,7 @@ public async Task Invoke(HttpContext httpContext) catch (Exception ex) { // TODO Review the error handling, we should throw an exception here and use the global error handler middleware to catch it - httpContext.Items.UpsertErrors([new UnmappableRequestError(ex)]); + httpContext.Items.SetError(new UnmappableRequestError(ex)); return; } diff --git a/src/Ocelot/Requester/HttpExceptionToErrorMapper.cs b/src/Ocelot/Requester/HttpExceptionToErrorMapper.cs index dad0e856c1..0a841d616a 100644 --- a/src/Ocelot/Requester/HttpExceptionToErrorMapper.cs +++ b/src/Ocelot/Requester/HttpExceptionToErrorMapper.cs @@ -1,17 +1,22 @@ +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Ocelot.Errors; using Ocelot.Errors.QoS; +using Ocelot.Request.Mapper; namespace Ocelot.Requester { public class HttpExceptionToErrorMapper : IExceptionToErrorMapper { /// This is a dictionary of custom mappers for exceptions. - private readonly Dictionary> _mappers; + private readonly IDictionary> _mappers; + + /// 413 status. + private const int RequestEntityTooLarge = (int)HttpStatusCode.RequestEntityTooLarge; public HttpExceptionToErrorMapper(IServiceProvider serviceProvider) { - _mappers = serviceProvider.GetService>>(); + _mappers = serviceProvider.GetService>>(); } public Error Map(Exception exception) @@ -39,6 +44,13 @@ public Error Map(Exception exception) if (type == typeof(HttpRequestException) || type == typeof(TimeoutException)) { + // Inner exception is a BadHttpRequestException, and only this exception exposes the StatusCode property. + // We check if the inner exception is a BadHttpRequestException and if the StatusCode is 413, we return a PayloadTooLargeError + if (exception.InnerException is BadHttpRequestException { StatusCode: RequestEntityTooLarge }) + { + return new PayloadTooLargeError(exception); + } + return new ConnectionToDownstreamServiceError(exception); } diff --git a/src/Ocelot/Requester/MessageInvokerPool.cs b/src/Ocelot/Requester/MessageInvokerPool.cs index 32d9b52353..130b4bccbf 100644 --- a/src/Ocelot/Requester/MessageInvokerPool.cs +++ b/src/Ocelot/Requester/MessageInvokerPool.cs @@ -6,11 +6,6 @@ namespace Ocelot.Requester; public class MessageInvokerPool : IMessageInvokerPool { - /// - /// TODO This should be configurable and available as global config parameter in ocelot.json. - /// - public const int DefaultRequestTimeoutSeconds = 90; - private readonly ConcurrentDictionary> _handlersPool; private readonly IDelegatingHandlerHandlerFactory _handlerFactory; private readonly IOcelotLogger _logger; @@ -37,6 +32,18 @@ public HttpMessageInvoker Get(DownstreamRoute downstreamRoute) public void Clear() => _handlersPool.Clear(); + /// + /// TODO This should be configurable and available as global config parameter in ocelot.json. + /// + public const int DefaultRequestTimeoutSeconds = 90; + private int _requestTimeoutSeconds; + + public int RequestTimeoutSeconds + { + get => _requestTimeoutSeconds > 0 ? _requestTimeoutSeconds : DefaultRequestTimeoutSeconds; + set => _requestTimeoutSeconds = value > 0 ? value : DefaultRequestTimeoutSeconds; + } + private HttpMessageInvoker CreateMessageInvoker(DownstreamRoute downstreamRoute) { var baseHandler = CreateHandler(downstreamRoute); @@ -52,7 +59,7 @@ private HttpMessageInvoker CreateMessageInvoker(DownstreamRoute downstreamRoute) // Adding timeout handler to the top of the chain. // It's standard behavior to throw TimeoutException after the defined timeout (90 seconds by default) var timeoutHandler = new TimeoutDelegatingHandler(downstreamRoute.QosOptions.TimeoutValue == 0 - ? TimeSpan.FromSeconds(DefaultRequestTimeoutSeconds) + ? TimeSpan.FromSeconds(RequestTimeoutSeconds) : TimeSpan.FromMilliseconds(downstreamRoute.QosOptions.TimeoutValue)) { InnerHandler = baseHandler, @@ -93,9 +100,14 @@ private HttpMessageHandler CreateHandler(DownstreamRoute downstreamRoute) return handler; } - private readonly struct MessageInvokerCacheKey(DownstreamRoute downstreamRoute) : IEquatable + private readonly struct MessageInvokerCacheKey : IEquatable { - public DownstreamRoute DownstreamRoute { get; } = downstreamRoute; + public MessageInvokerCacheKey(DownstreamRoute downstreamRoute) + { + DownstreamRoute = downstreamRoute; + } + + public DownstreamRoute DownstreamRoute { get; } public override bool Equals(object obj) => obj is MessageInvokerCacheKey key && Equals(key); diff --git a/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs b/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs index e26d6a5b3f..2855437f05 100644 --- a/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs +++ b/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs @@ -39,15 +39,19 @@ public async Task Invoke(HttpContext httpContext) private void CreateLogBasedOnResponse(Response response) { - if (response.Data?.StatusCode <= HttpStatusCode.BadRequest) + var status = response.Data?.StatusCode ?? HttpStatusCode.Processing; + var reason = response.Data?.ReasonPhrase ?? "unknown"; + var uri = response.Data?.RequestMessage?.RequestUri?.ToString() ?? string.Empty; + + string message() => $"{(int)status} ({reason}) status code of request URI: {uri}."; + + if (status < HttpStatusCode.BadRequest) { - Logger.LogInformation(() => - $"{(int)response.Data.StatusCode} ({response.Data.ReasonPhrase}) status code, request uri: {response.Data.RequestMessage?.RequestUri}"); + Logger.LogInformation(message); } - else if (response.Data?.StatusCode >= HttpStatusCode.BadRequest) + else { - Logger.LogWarning( - () => $"{(int)response.Data.StatusCode} ({response.Data.ReasonPhrase}) status code, request uri: {response.Data.RequestMessage?.RequestUri}"); + Logger.LogWarning(message); } } } diff --git a/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs b/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs index 2d0e9e8c2e..b633e6d9ba 100644 --- a/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs +++ b/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs @@ -55,6 +55,11 @@ public int Map(List errors) return 500; } + if (errors.Any(e => e.Code == OcelotErrorCode.PayloadTooLargeError)) + { + return 413; + } + return 404; } } diff --git a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryFinderDelegate.cs b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryFinderDelegate.cs index c6e500925d..e6c49a7d62 100644 --- a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryFinderDelegate.cs +++ b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryFinderDelegate.cs @@ -1,7 +1,6 @@ using Ocelot.Configuration; using Ocelot.ServiceDiscovery.Providers; -namespace Ocelot.ServiceDiscovery -{ - public delegate IServiceDiscoveryProvider ServiceDiscoveryFinderDelegate(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamRoute route); -} +namespace Ocelot.ServiceDiscovery; + +public delegate IServiceDiscoveryProvider ServiceDiscoveryFinderDelegate(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamRoute route); diff --git a/test/Ocelot.AcceptanceTests/AggregateTests.cs b/test/Ocelot.AcceptanceTests/AggregateTests.cs index 7cb4c03e39..6257e27e6c 100644 --- a/test/Ocelot.AcceptanceTests/AggregateTests.cs +++ b/test/Ocelot.AcceptanceTests/AggregateTests.cs @@ -1,40 +1,59 @@ +using IdentityServer4.AccessTokenValidation; +using IdentityServer4.Extensions; +using IdentityServer4.Models; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Authorization; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.AcceptanceTests.Authentication; using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; using Ocelot.Middleware; using Ocelot.Multiplexer; +using System.Text; namespace Ocelot.AcceptanceTests { - public class AggregateTests : IDisposable + public sealed class AggregateTests : Steps, IDisposable { - private readonly Steps _steps; - private string _downstreamPathOne; - private string _downstreamPathTwo; private readonly ServiceHandler _serviceHandler; + private readonly string[] _downstreamPaths; public AggregateTests() { _serviceHandler = new ServiceHandler(); - _steps = new Steps(); + _downstreamPaths = new string[3]; + } + + public override void Dispose() + { + _serviceHandler.Dispose(); + base.Dispose(); } [Fact] - public void should_fix_issue_597() + [Trait("Issue", "597")] + public void Should_fix_issue_597() { var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { - Routes = new List + Routes = new() { - new() + new FileRoute { DownstreamPathTemplate = "/api/values?MailId={userid}", UpstreamPathTemplate = "/key1data/{userid}", - UpstreamHttpMethod = new List {"Get"}, + UpstreamHttpMethod = new() { "Get" }, DownstreamScheme = "http", - DownstreamHostAndPorts = new List + DownstreamHostAndPorts = new() { - new() + new FileHostAndPort { Host = "localhost", Port = port, @@ -42,15 +61,15 @@ public void should_fix_issue_597() }, Key = "key1", }, - new() + new FileRoute { DownstreamPathTemplate = "/api/values?MailId={userid}", UpstreamPathTemplate = "/key2data/{userid}", - UpstreamHttpMethod = new List {"Get"}, + UpstreamHttpMethod = new() { "Get" }, DownstreamScheme = "http", - DownstreamHostAndPorts = new List + DownstreamHostAndPorts = new() { - new() + new FileHostAndPort { Host = "localhost", Port = port, @@ -58,15 +77,15 @@ public void should_fix_issue_597() }, Key = "key2", }, - new() + new FileRoute { DownstreamPathTemplate = "/api/values?MailId={userid}", UpstreamPathTemplate = "/key3data/{userid}", - UpstreamHttpMethod = new List {"Get"}, + UpstreamHttpMethod = new() { "Get" }, DownstreamScheme = "http", - DownstreamHostAndPorts = new List + DownstreamHostAndPorts = new() { - new() + new FileHostAndPort { Host = "localhost", Port = port, @@ -74,15 +93,15 @@ public void should_fix_issue_597() }, Key = "key3", }, - new() + new FileRoute { DownstreamPathTemplate = "/api/values?MailId={userid}", UpstreamPathTemplate = "/key4data/{userid}", - UpstreamHttpMethod = new List {"Get"}, + UpstreamHttpMethod = new() { "Get" }, DownstreamScheme = "http", - DownstreamHostAndPorts = new List + DownstreamHostAndPorts = new() { - new() + new FileHostAndPort { Host = "localhost", Port = port, @@ -91,24 +110,16 @@ public void should_fix_issue_597() Key = "key4", }, }, - Aggregates = new List + Aggregates = new() { - new() + new FileAggregateRoute { - RouteKeys = new List{ - "key1", - "key2", - "key3", - "key4", - }, + RouteKeys = new() { "key1", "key2", "key3", "key4" }, UpstreamPathTemplate = "/EmpDetail/IN/{userid}", }, - new() + new FileAggregateRoute { - RouteKeys = new List{ - "key1", - "key2", - }, + RouteKeys = new() { "key1", "key2" }, UpstreamPathTemplate = "/EmpDetail/US/{userid}", }, }, @@ -121,92 +132,89 @@ public void should_fix_issue_597() var expected = "{\"key1\":some_data,\"key2\":some_data}"; this.Given(x => x.GivenServiceIsRunning($"http://localhost:{port}", 200, "some_data")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/EmpDetail/US/1")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/EmpDetail/US/1")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(expected)) .BDDfy(); } [Fact] - public void should_return_response_200_with_advanced_aggregate_configs() + public void Should_return_response_200_with_advanced_aggregate_configs() { var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var port3 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { - Routes = new List + Routes = new() + { + new FileRoute { - new() + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new() { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List + new FileHostAndPort { - new() - { - Host = "localhost", - Port = port1, - }, + Host = "localhost", + Port = port1, }, - UpstreamPathTemplate = "/Comments", - UpstreamHttpMethod = new List { "Get" }, - Key = "Comments", }, - new() + UpstreamPathTemplate = "/Comments", + UpstreamHttpMethod = new() { "Get" }, + Key = "Comments", + }, + new FileRoute + { + DownstreamPathTemplate = "/users/{userId}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new() { - DownstreamPathTemplate = "/users/{userId}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List + new FileHostAndPort { - new() - { - Host = "localhost", - Port = port2, - }, + Host = "localhost", + Port = port2, }, - UpstreamPathTemplate = "/UserDetails", - UpstreamHttpMethod = new List { "Get" }, - Key = "UserDetails", }, - new() + UpstreamPathTemplate = "/UserDetails/{userId}", + UpstreamHttpMethod = new() { "Get" }, + Key = "UserDetails", + }, + new FileRoute + { + DownstreamPathTemplate = "/posts/{postId}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new() { - DownstreamPathTemplate = "/posts/{postId}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List + new FileHostAndPort { - new() - { - Host = "localhost", - Port = port3, - }, + Host = "localhost", + Port = port3, }, - UpstreamPathTemplate = "/PostDetails", - UpstreamHttpMethod = new List { "Get" }, - Key = "PostDetails", }, + UpstreamPathTemplate = "/PostDetails/{postId}", + UpstreamHttpMethod = new() { "Get" }, + Key = "PostDetails", }, - Aggregates = new List + }, + Aggregates = new() + { + new FileAggregateRoute { - new() + UpstreamPathTemplate = "/", + UpstreamHost = "localhost", + RouteKeys = new() { "Comments", "UserDetails", "PostDetails" }, + RouteKeysConfig = new() { - UpstreamPathTemplate = "/", - UpstreamHost = "localhost", - RouteKeys = new List - { - "Comments", - "UserDetails", - "PostDetails", - }, - RouteKeysConfig = new List() - { - new(){RouteKey = "UserDetails",JsonPath = "$[*].writerId",Parameter = "userId"}, - new(){RouteKey = "PostDetails",JsonPath = "$[*].postId",Parameter = "postId"}, - }, + new AggregateRouteConfig + { RouteKey = "UserDetails", JsonPath = "$[*].writerId", Parameter = "userId" }, + new AggregateRouteConfig + { RouteKey = "PostDetails", JsonPath = "$[*].postId", Parameter = "postId" }, }, }, + }, }; var userDetailsResponseContent = @"{""id"":1,""firstName"":""abolfazl"",""lastName"":""rajabpour""}"; @@ -215,446 +223,585 @@ public void should_return_response_200_with_advanced_aggregate_configs() var expected = "{\"Comments\":" + commentsResponseContent + ",\"UserDetails\":" + userDetailsResponseContent + ",\"PostDetails\":" + postDetailsResponseContent + "}"; - this.Given(x => x.GivenServiceOneIsRunning($"http://localhost:{port1}", "/", 200, commentsResponseContent)) - .Given(x => x.GivenServiceTwoIsRunning($"http://localhost:{port2}", "/users/1", 200, userDetailsResponseContent)) - .Given(x => x.GivenServiceTwoIsRunning($"http://localhost:{port3}", "/posts/2", 200, postDetailsResponseContent)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) + this.Given(x => x.GivenServiceIsRunning(0, port1, "/", 200, commentsResponseContent)) + .Given(x => x.GivenServiceIsRunning(1, port2, "/users/1", 200, userDetailsResponseContent)) + .Given(x => x.GivenServiceIsRunning(2, port3, "/posts/2", 200, postDetailsResponseContent)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(expected)) .BDDfy(); } [Fact] - public void should_return_response_200_with_simple_url_user_defined_aggregate() + public void Should_return_response_200_with_simple_url_user_defined_aggregate() { var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { - Routes = new List + Routes = new() + { + new FileRoute { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port1, - }, - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura", - }, - new() + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new() { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List + new FileHostAndPort { - new() - { - Host = "localhost", - Port = port2, - }, + Host = "localhost", + Port = port1, }, - UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = new List { "Get" }, - Key = "Tom", }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new() { "Get" }, + Key = "Laura", }, - Aggregates = new List + + new FileRoute { - new() + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new() { - UpstreamPathTemplate = "/", - UpstreamHost = "localhost", - RouteKeys = new List + new FileHostAndPort { - "Laura", - "Tom", + Host = "localhost", + Port = port2, }, - Aggregator = "FakeDefinedAggregator", }, + UpstreamPathTemplate = "/tom", + UpstreamHttpMethod = new() { "Get" }, + Key = "Tom", + }, + }, + Aggregates = new() + { + new FileAggregateRoute + { + UpstreamPathTemplate = "/", + UpstreamHost = "localhost", + RouteKeys = new() { "Laura", "Tom" }, + Aggregator = "FakeDefinedAggregator", }, + }, }; var expected = "Bye from Laura, Bye from Tom"; - this.Given(x => x.GivenServiceOneIsRunning($"http://localhost:{port1}", "/", 200, "{Hello from Laura}")) - .Given(x => x.GivenServiceTwoIsRunning($"http://localhost:{port2}", "/", 200, "{Hello from Tom}")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithSpecificAggregatorsRegisteredInDi()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) + this.Given(x => x.GivenServiceIsRunning(0, port1, "/", 200, "{Hello from Laura}")) + .Given(x => x.GivenServiceIsRunning(1, port2, "/", 200, "{Hello from Tom}")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithSpecificAggregatorsRegisteredInDi()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(expected)) .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) .BDDfy(); } [Fact] - public void should_return_response_200_with_simple_url() + public void Should_return_response_200_with_simple_url() { var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port1, - }, - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura", - }, - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port2, - }, - }, - UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = new List { "Get" }, - Key = "Tom", - }, - }, - Aggregates = new List - { - new() - { - UpstreamPathTemplate = "/", - UpstreamHost = "localhost", - RouteKeys = new List - { - "Laura", - "Tom", - }, - }, - }, - }; - - var expected = "{\"Laura\":{Hello from Laura},\"Tom\":{Hello from Tom}}"; - - this.Given(x => x.GivenServiceOneIsRunning($"http://localhost:{port1}", "/", 200, "{Hello from Laura}")) - .Given(x => x.GivenServiceTwoIsRunning($"http://localhost:{port2}", "/", 200, "{Hello from Tom}")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) + var route1 = GivenRoute(port1, "/laura", "Laura"); + var route2 = GivenRoute(port2, "/tom", "Tom"); + var configuration = GivenConfiguration(route1, route2); + + this.Given(x => x.GivenServiceIsRunning(0, port1, "/", 200, "{Hello from Laura}")) + .Given(x => x.GivenServiceIsRunning(1, port2, "/", 200, "{Hello from Tom}")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("{\"Laura\":{Hello from Laura},\"Tom\":{Hello from Tom}}")) .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) .BDDfy(); } [Fact] - public void should_return_response_200_with_simple_url_one_service_404() + public void Should_return_response_200_with_simple_url_one_service_404() { var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { - Routes = new List + Routes = new() + { + new FileRoute { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port1, - }, - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura", - }, - new() + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new() { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List + new FileHostAndPort { - new() - { - Host = "localhost", - Port = port2, - }, + Host = "localhost", + Port = port1, }, - UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = new List { "Get" }, - Key = "Tom", }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new() { "Get" }, + Key = "Laura", }, - Aggregates = new List + new FileRoute { - new() + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new() { - UpstreamPathTemplate = "/", - UpstreamHost = "localhost", - RouteKeys = new List + new FileHostAndPort { - "Laura", - "Tom", + Host = "localhost", + Port = port2, }, }, + UpstreamPathTemplate = "/tom", + UpstreamHttpMethod = new() { "Get" }, + Key = "Tom", + }, + }, + Aggregates = new() + { + new FileAggregateRoute + { + UpstreamPathTemplate = "/", + UpstreamHost = "localhost", + RouteKeys = new() { "Laura", "Tom" }, }, + }, }; var expected = "{\"Laura\":,\"Tom\":{Hello from Tom}}"; - this.Given(x => x.GivenServiceOneIsRunning($"http://localhost:{port1}", "/", 404, "")) - .Given(x => x.GivenServiceTwoIsRunning($"http://localhost:{port2}", "/", 200, "{Hello from Tom}")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) + this.Given(x => x.GivenServiceIsRunning(0, port1, "/", 404, "")) + .Given(x => x.GivenServiceIsRunning(1, port2, "/", 200, "{Hello from Tom}")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(expected)) .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) .BDDfy(); } [Fact] - public void should_return_response_200_with_simple_url_both_service_404() + public void Should_return_response_200_with_simple_url_both_service_404() { var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { - Routes = new List + Routes = new() + { + new FileRoute { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port1, - }, - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura", - }, - new() + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new() { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List + new FileHostAndPort { - new() - { - Host = "localhost", - Port = port2, - }, + Host = "localhost", + Port = port1, }, - UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = new List { "Get" }, - Key = "Tom", }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new() { "Get" }, + Key = "Laura", }, - Aggregates = new List + new FileRoute { - new() + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new() { - UpstreamPathTemplate = "/", - UpstreamHost = "localhost", - RouteKeys = new List + new FileHostAndPort { - "Laura", - "Tom", + Host = "localhost", + Port = port2, }, }, + UpstreamPathTemplate = "/tom", + UpstreamHttpMethod = new() { "Get" }, + Key = "Tom", }, + }, + Aggregates = new() + { + new FileAggregateRoute + { + UpstreamPathTemplate = "/", + UpstreamHost = "localhost", + RouteKeys = new() { "Laura", "Tom" }, + }, + }, }; var expected = "{\"Laura\":,\"Tom\":}"; - this.Given(x => x.GivenServiceOneIsRunning($"http://localhost:{port1}", "/", 404, "")) - .Given(x => x.GivenServiceTwoIsRunning($"http://localhost:{port2}", "/", 404, "")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) + this.Given(x => x.GivenServiceIsRunning(0, port1, "/", 404, "")) + .Given(x => x.GivenServiceIsRunning(1, port2, "/", 404, "")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(expected)) .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) .BDDfy(); } [Fact] - public void should_be_thread_safe() + public void Should_be_thread_safe() { var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { - Routes = new List + Routes = new() + { + new FileRoute { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port1, - }, - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura", - }, - new() + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new() { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List + new FileHostAndPort { - new() - { - Host = "localhost", - Port = port2, - }, + Host = "localhost", + Port = port1, }, - UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = new List { "Get" }, - Key = "Tom", }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new() { "Get" }, + Key = "Laura", }, - Aggregates = new List + new FileRoute { - new() + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new() { - UpstreamPathTemplate = "/", - UpstreamHost = "localhost", - RouteKeys = new List + new FileHostAndPort { - "Laura", - "Tom", + Host = "localhost", + Port = port2, }, }, + UpstreamPathTemplate = "/tom", + UpstreamHttpMethod = new() { "Get" }, + Key = "Tom", + }, + }, + Aggregates = new() + { + new FileAggregateRoute + { + UpstreamPathTemplate = "/", + UpstreamHost = "localhost", + RouteKeys = new() { "Laura", "Tom" }, }, + }, }; - this.Given(x => x.GivenServiceOneIsRunning($"http://localhost:{port1}", "/", 200, "{Hello from Laura}")) - .Given(x => x.GivenServiceTwoIsRunning($"http://localhost:{port2}", "/", 200, "{Hello from Tom}")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIMakeLotsOfDifferentRequestsToTheApiGateway()) + this.Given(x => x.GivenServiceIsRunning(0, port1, "/", 200, "{Hello from Laura}")) + .Given(x => x.GivenServiceIsRunning(1, port2, "/", 200, "{Hello from Tom}")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIMakeLotsOfDifferentRequestsToTheApiGateway()) .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) .BDDfy(); } - private void GivenServiceIsRunning(string baseUrl, int statusCode, string responseBody) + [Fact] + [Trait("Bug", "1396")] + public void Should_return_response_200_with_user_forwarding() { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, async context => + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var port3 = PortFinder.GetRandomPort(); + var route1 = GivenRoute(port1, "/laura", "Laura"); + var route2 = GivenRoute(port2, "/tom", "Tom"); + var configuration = GivenConfiguration(route1, route2); + var identityServerUrl = $"{Uri.UriSchemeHttp}://localhost:{port3}"; + Action options = o => { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - }); - } - - private void GivenServiceOneIsRunning(string baseUrl, string basePath, int statusCode, string responseBody) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + o.Authority = identityServerUrl; + o.ApiName = "api"; + o.RequireHttpsMetadata = false; + o.SupportedTokens = SupportedTokens.Both; + o.ApiSecret = "secret"; + o.ForwardDefault = IdentityServerAuthenticationDefaults.AuthenticationScheme; + }; + Action configureServices = s => { - _downstreamPathOne = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - - if (_downstreamPathOne != basePath) + s.AddOcelot(); + s.AddMvcCore(options => { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - else + var policy = new AuthorizationPolicyBuilder() + .RequireAuthenticatedUser() + .RequireClaim("scope", "api") + .Build(); + options.Filters.Add(new AuthorizeFilter(policy)); + }); + s.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme) + .AddIdentityServerAuthentication(options); + }; + var count = 0; + var actualContexts = new HttpContext[2]; + Action configureApp = async (app) => + { + var configuration = new OcelotPipelineConfiguration { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - } - }); + PreErrorResponderMiddleware = async (context, next) => + { + var auth = await context.AuthenticateAsync(); + context.User = (auth.Succeeded && auth.Principal?.IsAuthenticated() == true) + ? auth.Principal : null; + await next.Invoke(); + }, + AuthorizationMiddleware = (context, next) => + { + actualContexts[count++] = context; + return next.Invoke(); + }, + }; + await app.UseOcelot(configuration); + }; + using (var auth = new AuthenticationTests()) + { + this.Given(x => auth.GivenThereIsAnIdentityServerOn(identityServerUrl, AccessTokenType.Jwt)) + .And(x => x.GivenServiceIsRunning(0, port1, "/", 200, "{Hello from Laura}")) + .And(x => x.GivenServiceIsRunning(1, port2, "/", 200, "{Hello from Tom}")) + .And(x => auth.GivenIHaveAToken(identityServerUrl)) + .And(x => auth.GivenThereIsAConfiguration(configuration)) + .And(x => auth.GivenOcelotIsRunningWithServices(configureServices, configureApp)) + .And(x => auth.GivenIHaveAddedATokenToMyRequest()) + .When(x => auth.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => auth.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => auth.ThenTheResponseBodyShouldBe("{\"Laura\":{Hello from Laura},\"Tom\":{Hello from Tom}}")) + .And(x => x.ThenTheDownstreamUrlPathShouldBe("/", "/")) + .BDDfy(); + } + + // Assert + for (var i = 0; i < actualContexts.Length; i++) + { + var ctx = actualContexts[i].ShouldNotBeNull(); + ctx.Items.DownstreamRoute().Key.ShouldBe(configuration.Routes[i].Key); + var user = ctx.User.ShouldNotBeNull(); + user.IsAuthenticated().ShouldBeTrue(); + user.Claims.Count().ShouldBeGreaterThan(1); + user.Claims.FirstOrDefault(c => c is { Type: "scope", Value: "api" }).ShouldNotBeNull(); + } } - private void GivenServiceTwoIsRunning(string baseUrl, string basePath, int statusCode, string responseBody) + [Fact] + [Trait("Bug", "2039")] + public void Should_return_response_200_with_copied_body_sent_on_multiple_services() { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - _downstreamPathTwo = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var route1 = GivenRoute(port1, "/Service1", "Service1", "/Sub1"); + var route2 = GivenRoute(port2, "/Service2", "Service2", "/Sub2"); + var configuration = GivenConfiguration(route1, route2); + var requestBody = @"{""id"":1,""response"":""fromBody-#REPLACESTRING#""}"; + var sub1ResponseContent = @"{""id"":1,""response"":""fromBody-s1""}"; + var sub2ResponseContent = @"{""id"":1,""response"":""fromBody-s2""}"; + var expected = $"{{\"Service1\":{sub1ResponseContent},\"Service2\":{sub2ResponseContent}}}"; + + this.Given(x => x.GivenServiceIsRunning(0, port1, "/Sub1", 200, reqBody => reqBody.Replace("#REPLACESTRING#", "s1"))) + .Given(x => x.GivenServiceIsRunning(1, port2, "/Sub2", 200, reqBody => reqBody.Replace("#REPLACESTRING#", "s2"))) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlWithBodyOnTheApiGateway("/", requestBody)) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(expected)) + .BDDfy(); + } + + [Fact] + [Trait("Bug", "2039")] + public void Should_return_response_200_with_copied_form_sent_on_multiple_services() + { + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var route1 = GivenRoute(port1, "/Service1", "Service1", "/Sub1"); + var route2 = GivenRoute(port2, "/Service2", "Service2", "/Sub2"); + var configuration = GivenConfiguration(route1, route2); + + var formValues = new[] + { + new KeyValuePair("param1", "value1"), + new KeyValuePair("param2", "from-form-REPLACESTRING"), + }; + + var sub1ResponseContent = "\"[key:param1=value1¶m2=from-form-s1]\""; + var sub2ResponseContent = "\"[key:param1=value1¶m2=from-form-s2]\""; + var expected = $"{{\"Service1\":{sub1ResponseContent},\"Service2\":{sub2ResponseContent}}}"; + + this.Given(x => x.GivenServiceIsRunning(0, port1, "/Sub1", 200, (IFormCollection reqForm) => FormatFormCollection(reqForm).Replace("REPLACESTRING", "s1"))) + .Given(x => x.GivenServiceIsRunning(1, port2, "/Sub2", 200, (IFormCollection reqForm) => FormatFormCollection(reqForm).Replace("REPLACESTRING", "s2"))) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlWithFormOnTheApiGateway("/", "key", formValues)) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(expected)) + .BDDfy(); + } + + private static string FormatFormCollection(IFormCollection reqForm) + { + var sb = new StringBuilder() + .Append('"'); + + foreach (var kvp in reqForm) + { + sb.Append($"[{kvp.Key}:{kvp.Value}]"); + } + + return sb + .Append('"') + .ToString(); + } + + private void GivenServiceIsRunning(string baseUrl, int statusCode, string responseBody) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, async context => + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + } - if (_downstreamPathTwo != basePath) + private void GivenServiceIsRunning(int index, int port, string basePath, int statusCode, string responseBody) + => GivenServiceIsRunning(index, port, basePath, statusCode, + async context => + { + await context.Response.WriteAsync(responseBody); + }); + + private void GivenServiceIsRunning(int index, int port, string basePath, int statusCode, Func responseFromBody) + => GivenServiceIsRunning(index, port, basePath, statusCode, + async context => { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - else + var requestBody = await new StreamReader(context.Request.Body).ReadToEndAsync(); + var responseBody = responseFromBody(requestBody); + await context.Response.WriteAsync(responseBody); + }); + + private void GivenServiceIsRunning(int index, int port, string basePath, int statusCode, Func responseFromForm) + => GivenServiceIsRunning(index, port, basePath, statusCode, + async context => { - context.Response.StatusCode = statusCode; + var responseBody = responseFromForm(context.Request.Form); await context.Response.WriteAsync(responseBody); - } - }); + }); + + private void GivenServiceIsRunning(int index, int port, string basePath, int statusCode, Action processContext) + { + var baseUrl = DownstreamUrl(port); + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + _downstreamPaths[index] = !string.IsNullOrEmpty(context.Request.PathBase.Value) + ? context.Request.PathBase.Value + : context.Request.Path.Value; + + if (_downstreamPaths[index] != basePath) + { + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + await context.Response.WriteAsync("downstream path doesn't match base path"); + } + else + { + context.Response.StatusCode = statusCode; + processContext?.Invoke(context); + } + }); } - internal void ThenTheDownstreamUrlPathShouldBe(string expectedDownstreamPathOne, string expectedDownstreamPath) - { - _downstreamPathOne.ShouldBe(expectedDownstreamPathOne); - _downstreamPathTwo.ShouldBe(expectedDownstreamPath); + private void GivenOcelotIsRunningWithSpecificAggregatorsRegisteredInDi() + where TAggregator : class, IDefinedAggregator + where TDependency : class + { + _webHostBuilder = new WebHostBuilder(); + + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, true, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + s.AddSingleton(); + s.AddOcelot() + .AddSingletonDefinedAggregator(); + }) + .Configure(a => { a.UseOcelot().Wait(); }); + + _ocelotServer = new TestServer(_webHostBuilder); + _ocelotClient = _ocelotServer.CreateClient(); } - public void Dispose() - { - _serviceHandler.Dispose(); - _steps.Dispose(); + private void ThenTheDownstreamUrlPathShouldBe(string expectedDownstreamPathOne, string expectedDownstreamPath) + { + _downstreamPaths[0].ShouldBe(expectedDownstreamPathOne); + _downstreamPaths[1].ShouldBe(expectedDownstreamPath); } - } - public class FakeDepdendency - { + private static FileRoute GivenRoute(int port, string upstream, string key, string downstream = null) => new() + { + DownstreamPathTemplate = downstream ?? "/", + DownstreamScheme = Uri.UriSchemeHttp, + DownstreamHostAndPorts = new() { new("localhost", port) }, + UpstreamPathTemplate = upstream, + UpstreamHttpMethod = new() { HttpMethods.Get }, + Key = key, + }; + + private static new FileConfiguration GivenConfiguration(params FileRoute[] routes) + { + var obj = Steps.GivenConfiguration(routes); + obj.Aggregates.Add( + new() + { + UpstreamPathTemplate = "/", + UpstreamHost = "localhost", + RouteKeys = routes.Select(r => r.Key).ToList(), // [ "Laura", "Tom" ], + } + ); + return obj; + } } - public class FakeDefinedAggregator : IDefinedAggregator - { - private readonly FakeDepdendency _dep; - - public FakeDefinedAggregator(FakeDepdendency dep) - { - _dep = dep; - } - - public async Task Aggregate(List responses) - { - var one = await responses[0].Items.DownstreamResponse().Content.ReadAsStringAsync(); - var two = await responses[1].Items.DownstreamResponse().Content.ReadAsStringAsync(); + public class FakeDep + { + } - var merge = $"{one}, {two}"; - merge = merge.Replace("Hello", "Bye").Replace("{", "").Replace("}", ""); - var headers = responses.SelectMany(x => x.Items.DownstreamResponse().Headers).ToList(); - return new DownstreamResponse(new StringContent(merge), HttpStatusCode.OK, headers, "some reason"); - } + public class FakeDefinedAggregator : IDefinedAggregator + { + public FakeDefinedAggregator(FakeDep dep) + { + } + + public async Task Aggregate(List responses) + { + var one = await responses[0].Items.DownstreamResponse().Content.ReadAsStringAsync(); + var two = await responses[1].Items.DownstreamResponse().Content.ReadAsStringAsync(); + + var merge = $"{one}, {two}"; + merge = merge.Replace("Hello", "Bye").Replace("{", "").Replace("}", ""); + var headers = responses.SelectMany(x => x.Items.DownstreamResponse().Headers).ToList(); + return new DownstreamResponse(new StringContent(merge), HttpStatusCode.OK, headers, "some reason"); + } } -} +} diff --git a/test/Ocelot.AcceptanceTests/Authentication/AuthenticationSteps.cs b/test/Ocelot.AcceptanceTests/Authentication/AuthenticationSteps.cs index 29e1cb2868..b9e626f000 100644 --- a/test/Ocelot.AcceptanceTests/Authentication/AuthenticationSteps.cs +++ b/test/Ocelot.AcceptanceTests/Authentication/AuthenticationSteps.cs @@ -57,7 +57,7 @@ protected static Client CreateClientWithSecret(string clientId, Secret secret, A protected static Client DefaultClient(AccessTokenType tokenType = AccessTokenType.Jwt, string[] apiScopes = null) { - apiScopes ??= ["api"]; + apiScopes ??= new string[] { "api" }; return new() { ClientId = "client", @@ -65,7 +65,7 @@ protected static Client DefaultClient(AccessTokenType tokenType = AccessTokenTyp ClientSecrets = new List { new("secret".Sha256()) }, AllowedScopes = apiScopes .Union(apiScopes.Select(x => $"{x}.readOnly")) - .Union(["openid", "offline_access"]) + .Union(new string[] { "openid", "offline_access" }) .ToList(), AccessTokenType = tokenType, Enabled = true, @@ -76,8 +76,8 @@ protected static Client DefaultClient(AccessTokenType tokenType = AccessTokenTyp public static IWebHostBuilder CreateIdentityServer(string url, AccessTokenType tokenType, string[] apiScopes, Client[] clients) { - apiScopes ??= ["api"]; - clients ??= [DefaultClient(tokenType, apiScopes)]; + apiScopes ??= new string[] { "api" }; + clients ??= new Client[] { DefaultClient(tokenType, apiScopes) }; var builder = new WebHostBuilder() .UseUrls(url) .UseKestrel() @@ -93,10 +93,10 @@ public static IWebHostBuilder CreateIdentityServer(string url, AccessTokenType t .Select(apiname => new ApiScope(apiname, apiname.ToUpper()))) .AddInMemoryApiResources(apiScopes .Select(x => new { i = Array.IndexOf(apiScopes, x), scope = x }) - .Select(x => CreateApiResource(x.scope, ["openid", "offline_access"]))) + .Select(x => CreateApiResource(x.scope, new string[] { "openid", "offline_access" }))) .AddInMemoryClients(clients) - .AddTestUsers( - [ + .AddTestUsers(new() + { new() { Username = "test", @@ -108,7 +108,7 @@ public static IWebHostBuilder CreateIdentityServer(string url, AccessTokenType t new("LocationId", "321"), }, }, - ]); + }); }) .Configure(app => { @@ -141,26 +141,19 @@ internal Task GivenAuthToken(string url, string apiScope, string cl public static FileRoute GivenDefaultAuthRoute(int port, string upstreamHttpMethod = null, string authProviderKey = null) => new() { DownstreamPathTemplate = "/", - DownstreamHostAndPorts = - [ - new("localhost", port), - ], + DownstreamHostAndPorts = new() + { + new("localhost", port), + }, DownstreamScheme = Uri.UriSchemeHttp, UpstreamPathTemplate = "/", - UpstreamHttpMethod = [upstreamHttpMethod ?? HttpMethods.Get], - AuthenticationOptions = new FileAuthenticationOptions + UpstreamHttpMethod = new() { upstreamHttpMethod ?? HttpMethods.Get }, + AuthenticationOptions = new() { - AuthenticationProviderKey = authProviderKey ?? "Test", + AuthenticationProviderKeys = new string[] { authProviderKey ?? "Test" }, }, }; - public static FileConfiguration GivenConfiguration(params FileRoute[] routes) - { - var configuration = new FileConfiguration(); - configuration.Routes.AddRange(routes); - return configuration; - } - protected void GivenThereIsAServiceRunningOn(int port, HttpStatusCode statusCode, string responseBody) { var url = DownstreamServiceUrl(port); diff --git a/test/Ocelot.AcceptanceTests/Authentication/AuthenticationTests.cs b/test/Ocelot.AcceptanceTests/Authentication/AuthenticationTests.cs index 111cd3afec..05a60e7e2c 100644 --- a/test/Ocelot.AcceptanceTests/Authentication/AuthenticationTests.cs +++ b/test/Ocelot.AcceptanceTests/Authentication/AuthenticationTests.cs @@ -112,7 +112,8 @@ public void Should_return_201_using_identity_server_reference_token() .BDDfy(); } - private void GivenThereIsAnIdentityServerOn(string url, AccessTokenType tokenType) + [IgnorePublicMethod] + public void GivenThereIsAnIdentityServerOn(string url, AccessTokenType tokenType) { var scopes = new string[] { "api", "api2" }; _identityServerBuilder = CreateIdentityServer(url, tokenType, scopes, null) @@ -127,4 +128,11 @@ public override void Dispose() base.Dispose(); } } + + [AttributeUsage(AttributeTargets.Class)] + public sealed class IgnoreXunitAnalyzersRule1013Attribute : Attribute { } + + [IgnoreXunitAnalyzersRule1013] + [AttributeUsage(AttributeTargets.Method)] + public class IgnorePublicMethodAttribute : Attribute { } } diff --git a/test/Ocelot.AcceptanceTests/Authentication/MultipleAuthSchemesFeatureTests.cs b/test/Ocelot.AcceptanceTests/Authentication/MultipleAuthSchemesFeatureTests.cs index 2044414971..f797431f96 100644 --- a/test/Ocelot.AcceptanceTests/Authentication/MultipleAuthSchemesFeatureTests.cs +++ b/test/Ocelot.AcceptanceTests/Authentication/MultipleAuthSchemesFeatureTests.cs @@ -19,9 +19,9 @@ public sealed class MultipleAuthSchemesFeatureTests : AuthenticationSteps, IDisp public MultipleAuthSchemesFeatureTests() : base() { - _identityServers = []; - _identityServerUrls = []; - _tokens = []; + _identityServers = Array.Empty(); + _identityServerUrls = Array.Empty(); + _tokens = Array.Empty(); } public override void Dispose() diff --git a/test/Ocelot.AcceptanceTests/Caching/CachingTests.cs b/test/Ocelot.AcceptanceTests/Caching/CachingTests.cs index 4eb8a5bf3c..c029ad56d7 100644 --- a/test/Ocelot.AcceptanceTests/Caching/CachingTests.cs +++ b/test/Ocelot.AcceptanceTests/Caching/CachingTests.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; +using System.Text; +using JsonSerializer = System.Text.Json.JsonSerializer; namespace Ocelot.AcceptanceTests.Caching { @@ -10,6 +12,7 @@ public sealed class CachingTests : IDisposable private const string HelloTomContent = "Hello from Tom"; private const string HelloLauraContent = "Hello from Laura"; + private int _counter = 0; public CachingTests() { @@ -113,6 +116,75 @@ public void Should_not_return_cached_response_as_ttl_expires() .BDDfy(); } + [Theory] + [InlineData(true)] + [InlineData(false)] + [Trait("Feat", "2058")] + [Trait("Bug", "2059")] + public void Should_return_different_cached_response_when_request_body_changes_and_EnableContentHashing_is_true(bool asGlobalConfig) + { + var port = PortFinder.GetRandomPort(); + var options = new FileCacheOptions + { + TtlSeconds = 100, + EnableContentHashing = true, + }; + var (testBody1String, testBody2String) = TestBodiesFactory(); + var configuration = GivenFileConfiguration(port, options, asGlobalConfig); + + this.Given(x => x.GivenThereIsAnEchoServiceRunningOn($"http://localhost:{port}")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody1String, Encoding.UTF8, "application/json"))) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(testBody1String)) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody2String, Encoding.UTF8, "application/json"))) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(testBody2String)) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody1String, Encoding.UTF8, "application/json"))) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(testBody1String)) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody2String, Encoding.UTF8, "application/json"))) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(testBody2String)) + .And(x => ThenTheCounterValueShouldBe(2)) + .BDDfy(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + [Trait("Feat", "2058")] + [Trait("Bug", "2059")] + public void Should_return_same_cached_response_when_request_body_changes_and_EnableContentHashing_is_false(bool asGlobalConfig) + { + var port = PortFinder.GetRandomPort(); + var options = new FileCacheOptions + { + TtlSeconds = 100, + }; + var (testBody1String, testBody2String) = TestBodiesFactory(); + var configuration = GivenFileConfiguration(port, options, asGlobalConfig); + + this.Given(x => x.GivenThereIsAnEchoServiceRunningOn($"http://localhost:{port}")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody1String, Encoding.UTF8, "application/json"))) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(testBody1String)) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody2String, Encoding.UTF8, "application/json"))) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(testBody1String)) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody1String, Encoding.UTF8, "application/json"))) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(testBody1String)) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody2String, Encoding.UTF8, "application/json"))) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(testBody1String)) + .And(x => ThenTheCounterValueShouldBe(1)) + .BDDfy(); + } + [Fact] [Trait("Issue", "1172")] public void Should_clean_cached_response_by_cache_header_via_new_caching_key() @@ -152,23 +224,25 @@ public void Should_clean_cached_response_by_cache_header_via_new_caching_key() .BDDfy(); } - private static FileConfiguration GivenFileConfiguration(int port, FileCacheOptions cacheOptions) => new() + private static FileConfiguration GivenFileConfiguration(int port, FileCacheOptions cacheOptions, bool asGlobalConfig = false) => new() { - Routes = - [ + Routes = new() + { new FileRoute() { DownstreamPathTemplate = "/", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort("localhost", port), - ], + }, + DownstreamHttpMethod = "Post", DownstreamScheme = Uri.UriSchemeHttp, UpstreamPathTemplate = "/", - UpstreamHttpMethod =["Get"], - FileCacheOptions = cacheOptions, + UpstreamHttpMethod = new() { HttpMethods.Get, HttpMethods.Post }, + FileCacheOptions = asGlobalConfig ? new FileCacheOptions { TtlSeconds = cacheOptions.TtlSeconds } : cacheOptions, }, - ], + }, + GlobalConfiguration = asGlobalConfig ? new FileGlobalConfiguration { CacheOptions = cacheOptions } : null, }; private static void GivenTheCacheExpires() @@ -196,10 +270,61 @@ private void GivenThereIsAServiceRunningOn(string url, HttpStatusCode statusCode }); } + private void GivenThereIsAnEchoServiceRunningOn(string url) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + using var streamReader = new StreamReader(context.Request.Body); + var requestBody = await streamReader.ReadToEndAsync(); + + _counter++; + context.Response.StatusCode = (int)HttpStatusCode.OK; + await context.Response.WriteAsync(requestBody); + }); + } + + private void ThenTheCounterValueShouldBe(int expected) + { + Assert.Equal(expected, _counter); + } + + private (string TestBody1String, string TestBody2String) TestBodiesFactory() + { + var testBody1 = new TestBody + { + Age = 30, + Email = "test.test@email.com", + FirstName = "Jean", + LastName = "Test", + }; + + var testBody1String = JsonSerializer.Serialize(testBody1); + + var testBody2 = new TestBody + { + Age = 31, + Email = "test.test@email.com", + FirstName = "Jean", + LastName = "Test", + }; + + var testBody2String = JsonSerializer.Serialize(testBody2); + + return (testBody1String, testBody2String); + } + public void Dispose() { _serviceHandler?.Dispose(); _steps.Dispose(); } } + + public class TestBody + { + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + public int Age { get; set; } + } } diff --git a/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs b/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs index b3697f0d67..04b0580d26 100644 --- a/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs +++ b/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs @@ -61,7 +61,7 @@ public void should_return_200_and_change_downstream_path() }, }, DownstreamScheme = "http", - UpstreamPathTemplate = "/users", + UpstreamPathTemplate = "/users/{userId}", UpstreamHttpMethod = new List { "Get" }, AuthenticationOptions = new FileAuthenticationOptions { diff --git a/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs b/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs deleted file mode 100644 index dad9af3dcf..0000000000 --- a/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs +++ /dev/null @@ -1,217 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration.File; - -namespace Ocelot.AcceptanceTests -{ - public class ClientRateLimitTests : IDisposable - { - private readonly Steps _steps; - private int _counterOne; - private readonly ServiceHandler _serviceHandler; - - public ClientRateLimitTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } - - [Fact] - public void should_call_withratelimiting() - { - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/ClientRateLimit", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/api/ClientRateLimit", - UpstreamHttpMethod = new List { "Get" }, - RequestIdKey = _steps.RequestIdKey, - RateLimitOptions = new FileRateLimitRule - { - EnableRateLimiting = true, - ClientWhitelist = new List(), - Limit = 3, - Period = "1s", - PeriodTimespan = 1000, - }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - RateLimitOptions = new FileRateLimitOptions - { - ClientIdHeader = "ClientId", - DisableRateLimitHeaders = false, - QuotaExceededMessage = string.Empty, - RateLimitCounterPrefix = string.Empty, - HttpStatusCode = 428, - }, - RequestIdKey = "oceclientrequest", - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/ClientRateLimit")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 2)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) - .BDDfy(); - } - - [Fact] - public void should_wait_for_period_timespan_to_elapse_before_making_next_request() - { - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/ClientRateLimit", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/api/ClientRateLimit", - UpstreamHttpMethod = new List { "Get" }, - RequestIdKey = _steps.RequestIdKey, - - RateLimitOptions = new FileRateLimitRule - { - EnableRateLimiting = true, - ClientWhitelist = new List(), - Limit = 3, - Period = "1s", - PeriodTimespan = 2, - }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - RateLimitOptions = new FileRateLimitOptions - { - ClientIdHeader = "ClientId", - DisableRateLimitHeaders = false, - QuotaExceededMessage = string.Empty, - RateLimitCounterPrefix = string.Empty, - HttpStatusCode = 428, - }, - RequestIdKey = "oceclientrequest", - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/ClientRateLimit")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 2)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) - .And(x => _steps.GivenIWait(1000)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) - .And(x => _steps.GivenIWait(1000)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .BDDfy(); - } - - [Fact] - public void should_call_middleware_withWhitelistClient() - { - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/ClientRateLimit", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/api/ClientRateLimit", - UpstreamHttpMethod = new List { "Get" }, - RequestIdKey = _steps.RequestIdKey, - - RateLimitOptions = new FileRateLimitRule - { - EnableRateLimiting = true, - ClientWhitelist = new List { "ocelotclient1"}, - Limit = 3, - Period = "1s", - PeriodTimespan = 100, - }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - RateLimitOptions = new FileRateLimitOptions - { - ClientIdHeader = "ClientId", - DisableRateLimitHeaders = false, - QuotaExceededMessage = string.Empty, - RateLimitCounterPrefix = string.Empty, - }, - RequestIdKey = "oceclientrequest", - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/ClientRateLimit")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 4)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .BDDfy(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, context => - { - _counterOne++; - context.Response.StatusCode = 200; - context.Response.WriteAsync(_counterOne.ToString()); - return Task.CompletedTask; - }); - } - - public void Dispose() - { - _steps.Dispose(); - } - } -} diff --git a/test/Ocelot.AcceptanceTests/ConfigurationMergeTests.cs b/test/Ocelot.AcceptanceTests/ConfigurationMergeTests.cs new file mode 100644 index 0000000000..3836dcbf63 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/ConfigurationMergeTests.cs @@ -0,0 +1,68 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; +using System.Runtime.CompilerServices; + +namespace Ocelot.AcceptanceTests; + +[Trait("PR", "1227")] +[Trait("Issue", "1216")] +public sealed class ConfigurationMergeTests : Steps +{ + private readonly FileConfiguration _globalConfig; + private readonly string _globalConfigFileName; + + public ConfigurationMergeTests() : base() + { + _globalConfig = new(); + _globalConfigFileName = $"{TestID}-{ConfigurationBuilderExtensions.GlobalConfigFile}"; + } + + protected override void DeleteOcelotConfig(params string[] files) => base.DeleteOcelotConfig(_globalConfigFileName); + + [Fact] + public void Should_run_with_global_config_merged_to_memory() + { + Arrange(); + + // Act + GivenOcelotIsRunningMergedConfig(MergeOcelotJson.ToMemory); + + // Assert + TheOcelotPrimaryConfigFileExists(false); + Assert(); + } + + [Fact] + public void Should_run_with_global_config_merged_to_file() + { + Arrange(); + + // Act + GivenOcelotIsRunningMergedConfig(MergeOcelotJson.ToFile); + + // Assert + TheOcelotPrimaryConfigFileExists(true); + Assert(); + } + + private void GivenOcelotIsRunningMergedConfig(MergeOcelotJson mergeTo) + => StartOcelot((context, config) => config.AddOcelot(_globalConfig, context.HostingEnvironment, mergeTo, _ocelotConfigFileName, _globalConfigFileName, null, false, false)); + + private void TheOcelotPrimaryConfigFileExists(bool expected) + => File.Exists(_ocelotConfigFileName).ShouldBe(expected); + + private void Arrange([CallerMemberName] string testName = null) + { + _globalConfig.GlobalConfiguration.RequestIdKey = testName; + } + + private void Assert([CallerMemberName] string testName = null) + { + var config = _ocelotServer.Services.GetService(); + config.ShouldNotBeNull(); + var actual = config["GlobalConfiguration:RequestIdKey"]; + actual.ShouldNotBeNull().ShouldBe(testName); + } +} diff --git a/test/Ocelot.AcceptanceTests/ContentTests.cs b/test/Ocelot.AcceptanceTests/ContentTests.cs index 5c64da21fb..272dc52100 100644 --- a/test/Ocelot.AcceptanceTests/ContentTests.cs +++ b/test/Ocelot.AcceptanceTests/ContentTests.cs @@ -170,20 +170,20 @@ private static string GenerateDummyDatFile(int sizeInMb) private static FileConfiguration GivenConfiguration(int port, string method = null) => new() { - Routes = - [ + Routes = new() + { new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = Uri.UriSchemeHttp, - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort("localhost", port), - ], + }, UpstreamPathTemplate = "/", - UpstreamHttpMethod = [method ?? HttpMethods.Get], + UpstreamHttpMethod = new() {method ?? HttpMethods.Get }, }, - ], + }, }; } } diff --git a/test/Ocelot.AcceptanceTests/HttpTests.cs b/test/Ocelot.AcceptanceTests/HttpTests.cs index 532bd6276b..778dcb1552 100644 --- a/test/Ocelot.AcceptanceTests/HttpTests.cs +++ b/test/Ocelot.AcceptanceTests/HttpTests.cs @@ -1,6 +1,8 @@ +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; using Ocelot.Configuration.File; +using System.Security.Authentication; namespace Ocelot.AcceptanceTests { @@ -216,25 +218,45 @@ public void should_return_response_200_when_using_http_two_to_talk_to_server_run } private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int port, HttpProtocols protocols) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + void options(KestrelServerOptions serverOptions) + { + serverOptions.Listen(IPAddress.Loopback, port, listenOptions => + { + listenOptions.Protocols = protocols; + }); + } + + _serviceHandler.GivenThereIsAServiceRunningOnWithKestrelOptions(baseUrl, basePath, options, async context => { context.Response.StatusCode = 200; var reader = new StreamReader(context.Request.Body); var body = await reader.ReadToEndAsync(); await context.Response.WriteAsync(body); - }, port, protocols); + }); } private void GivenThereIsAServiceUsingHttpsRunningOn(string baseUrl, string basePath, int port, HttpProtocols protocols) { - _serviceHandler.GivenThereIsAServiceRunningOnUsingHttps(baseUrl, basePath, async context => + void options(KestrelServerOptions serverOptions) + { + serverOptions.Listen(IPAddress.Loopback, port, listenOptions => + { + listenOptions.UseHttps("mycert.pfx", "password", options => + { + options.SslProtocols = SslProtocols.Tls12; + }); + listenOptions.Protocols = protocols; + }); + } + + _serviceHandler.GivenThereIsAServiceRunningOnWithKestrelOptions(baseUrl, basePath, options, async context => { context.Response.StatusCode = 200; var reader = new StreamReader(context.Request.Body); var body = await reader.ReadToEndAsync(); await context.Response.WriteAsync(body); - }, port, protocols); + }); } public void Dispose() diff --git a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj index 50fcbca1ce..b16d138488 100644 --- a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj +++ b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj @@ -14,9 +14,9 @@ false false false - ..\..\codeanalysis.ruleset True - 1591 + ..\..\codeanalysis.ruleset + $(NoWarn);CS0618;CS1591 @@ -53,22 +53,23 @@ PreserveNewest - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + @@ -94,6 +95,7 @@ + diff --git a/test/Ocelot.AcceptanceTests/PollyQoSTests.cs b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs index fda947c817..58481afa3e 100644 --- a/test/Ocelot.AcceptanceTests/PollyQoSTests.cs +++ b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs @@ -1,52 +1,57 @@ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.File; +using Ocelot.Requester; +using System.Reflection; namespace Ocelot.AcceptanceTests { - public class PollyQoSTests : IDisposable + public sealed class PollyQoSTests : Steps, IDisposable { - private readonly Steps _steps; private readonly ServiceHandler _serviceHandler; public PollyQoSTests() { _serviceHandler = new ServiceHandler(); - _steps = new Steps(); + } + + public override void Dispose() + { + _serviceHandler.Dispose(); + base.Dispose(); } - private static FileConfiguration FileConfigurationFactory(int port, QoSOptions options, string httpMethod = nameof(HttpMethods.Get)) - => new() + private static FileConfiguration FileConfigurationFactory(int port, QoSOptions options, string httpMethod = nameof(HttpMethods.Get)) => new() + { + Routes = new() { - Routes = new List + new() { - new() + DownstreamPathTemplate = "/", + DownstreamScheme = Uri.UriSchemeHttp, + DownstreamHostAndPorts = new() { - DownstreamPathTemplate = "/", - DownstreamScheme = Uri.UriSchemeHttp, - DownstreamHostAndPorts = new() - { - new("localhost", port), - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new() {httpMethod}, - QoSOptions = new FileQoSOptions(options), + new("localhost", port), }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new() {httpMethod}, + QoSOptions = new FileQoSOptions(options), }, - }; + }, + }; [Fact] public void Should_not_timeout() { var port = PortFinder.GetRandomPort(); - var configuration = FileConfigurationFactory(port, new QoSOptions(10, 0, 1000, null), HttpMethods.Post); + var configuration = FileConfigurationFactory(port, new QoSOptions(10, 500, 1000, null), HttpMethods.Post); this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, string.Empty, 10)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .And(x => _steps.GivenThePostHasContent("postContent")) - .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithPolly()) + .And(x => GivenThePostHasContent("postContent")) + .When(x => WhenIPostUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .BDDfy(); } @@ -54,14 +59,14 @@ public void Should_not_timeout() public void Should_timeout() { var port = PortFinder.GetRandomPort(); - var configuration = FileConfigurationFactory(port, new QoSOptions(0, 0, 10, null), HttpMethods.Post); - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 201, string.Empty, 1000)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .And(x => _steps.GivenThePostHasContent("postContent")) - .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + var configuration = FileConfigurationFactory(port, new QoSOptions(0, 0, 1000, null), HttpMethods.Post); + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 201, string.Empty, 2100)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithPolly()) + .And(x => GivenThePostHasContent("postContent")) + .When(x => WhenIPostUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) .BDDfy(); } @@ -72,12 +77,12 @@ public void Should_open_circuit_breaker_after_two_exceptions() var configuration = FileConfigurationFactory(port, new QoSOptions(2, 5000, 100000, null)); this.Given(x => x.GivenThereIsABrokenServiceRunningOn($"http://localhost:{port}")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithPolly()) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) .BDDfy(); } @@ -85,24 +90,27 @@ public void Should_open_circuit_breaker_after_two_exceptions() public void Should_open_circuit_breaker_then_close() { var port = PortFinder.GetRandomPort(); - var configuration = FileConfigurationFactory(port, new QoSOptions(1, 500, 1000, null)); + var configuration = FileConfigurationFactory(port, new QoSOptions(2, 500, 1000, null)); this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn($"http://localhost:{port}", "Hello from Laura")) - .Given(x => _steps.GivenThereIsAConfiguration(configuration)) - .Given(x => _steps.GivenOcelotIsRunningWithPolly()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => GivenThereIsAConfiguration(configuration)) + .Given(x => GivenOcelotIsRunningWithPolly()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .When(x => WhenIGetUrlOnTheApiGateway("/")) // repeat same request because min ExceptionsAllowedBeforeBreaking is 2 + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .Given(x => WhenIGetUrlOnTheApiGateway("/")) + .Given(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => WhenIGetUrlOnTheApiGateway("/")) + .Given(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => WhenIGetUrlOnTheApiGateway("/")) + .Given(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) .Given(x => GivenIWaitMilliseconds(3000)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } @@ -111,7 +119,7 @@ public void Open_circuit_should_not_effect_different_route() { var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); - var qos1 = new QoSOptions(1, 1000, 1000, null); + var qos1 = new QoSOptions(2, 500, 1000, null); var configuration = FileConfigurationFactory(port1, qos1); var route2 = configuration.Routes[0].Clone() as FileRoute; @@ -122,40 +130,55 @@ public void Open_circuit_should_not_effect_different_route() this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn($"http://localhost:{port1}", "Hello from Laura")) .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port2}", 200, "Hello from Tom", 0)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/working")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithPolly()) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => WhenIGetUrlOnTheApiGateway("/")) // repeat same request because min ExceptionsAllowedBeforeBreaking is 2 + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => WhenIGetUrlOnTheApiGateway("/working")) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Tom")) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) .And(x => GivenIWaitMilliseconds(3000)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] [Trait("Bug", "1833")] public void Should_timeout_per_default_after_90_seconds() - { + { + // Arrange var port = PortFinder.GetRandomPort(); - var configuration = FileConfigurationFactory(port, new QoSOptions(new FileQoSOptions()), HttpMethods.Get); - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 201, string.Empty, 95000)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .BDDfy(); - } + var configuration = FileConfigurationFactory(port, new QoSOptions(new FileQoSOptions()), HttpMethods.Get); + GivenThereIsAServiceRunningOn(DownstreamUrl(port), (int)HttpStatusCode.Created, string.Empty, 3500); // 3.5s > 3s -> ServiceUnavailable + GivenThereIsAConfiguration(configuration); + GivenOcelotIsRunningWithPolly(); + GivenIHackDefaultTimeoutValue(3); // after 3 secs -> Timeout exception aka request cancellation + + // Act + WhenIGetUrlOnTheApiGateway("/"); + + // Assert + ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); + } + + private void GivenIHackDefaultTimeoutValue(int defaultTimeoutSeconds) + { + var field = typeof(MessageInvokerPool).GetField("_requestTimeoutSeconds", BindingFlags.NonPublic | BindingFlags.Instance); + var service = _ocelotServer.Services.GetService(typeof(IMessageInvokerPool)); + field.SetValue(service, defaultTimeoutSeconds); // hack the value of default 90 seconds + } private static void GivenIWaitMilliseconds(int ms) => Thread.Sleep(ms); @@ -173,9 +196,16 @@ private void GivenThereIsAPossiblyBrokenServiceRunningOn(string url, string resp var requestCount = 0; _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => { - if (requestCount == 1) + if (requestCount == 2) { - await Task.Delay(1000); + // in Polly v8 + // MinimumThroughput (ExceptionsAllowedBeforeBreaking) must be 2 or more + // BreakDuration (DurationOfBreak) must be 500 or more + // Timeout (TimeoutValue) must be 1000 or more + // so we wait for 2.1 seconds to make sure the circuit is open + // DurationOfBreak * ExceptionsAllowedBeforeBreaking + Timeout + // 500 * 2 + 1000 = 2000 minimum + 100 milliseconds to exceed the minimum + await Task.Delay(2100); } requestCount++; @@ -193,12 +223,5 @@ private void GivenThereIsAServiceRunningOn(string url, int statusCode, string re await context.Response.WriteAsync(responseBody); }); } - - public void Dispose() - { - _serviceHandler?.Dispose(); - _steps.Dispose(); - GC.SuppressFinalize(this); - } } } diff --git a/test/Ocelot.AcceptanceTests/RateLimiting/ClientRateLimitingTests.cs b/test/Ocelot.AcceptanceTests/RateLimiting/ClientRateLimitingTests.cs new file mode 100644 index 0000000000..4dd80e7ec2 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/RateLimiting/ClientRateLimitingTests.cs @@ -0,0 +1,182 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; + +namespace Ocelot.AcceptanceTests.RateLimiting; + +public sealed class ClientRateLimitingTests : Steps, IDisposable +{ + const int OK = (int)HttpStatusCode.OK; + const int TooManyRequests = (int)HttpStatusCode.TooManyRequests; + + private int _counterOne; + private readonly ServiceHandler _serviceHandler; + + public ClientRateLimitingTests() + { + _serviceHandler = new ServiceHandler(); + } + + public override void Dispose() + { + _serviceHandler.Dispose(); + base.Dispose(); + } + + [Fact] + [Trait("Feat", "37")] + public void Should_call_with_rate_limiting() + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, null, null, new(), 3, "1s", 1); // periods are equal + var configuration = GivenConfigurationWithRateLimitOptions(route); + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(port), "/api/ClientRateLimit")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) + .Then(x => ThenTheStatusCodeShouldBe(OK)) + .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 2)) + .Then(x => ThenTheStatusCodeShouldBe(OK)) + .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) + .Then(x => ThenTheStatusCodeShouldBe(TooManyRequests)) + .BDDfy(); + } + + [Fact] + [Trait("Feat", "37")] + public void Should_wait_for_period_timespan_to_elapse_before_making_next_request() + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, "/api/ClientRateLimit?count={count}", "/ClientRateLimit/?{count}", new(), 3, "1s", 2); + var configuration = GivenConfigurationWithRateLimitOptions(route); + _counterOne = 0; + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(port), "/api/ClientRateLimit")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Url, 1)) + .Then(x => ThenTheStatusCodeShouldBe(OK)) + .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Url, 2)) + .Then(x => ThenTheStatusCodeShouldBe(OK)) + .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Url, 1)) + .Then(x => ThenTheStatusCodeShouldBe(TooManyRequests)) + .And(x => GivenIWait(1000)) + .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Url, 1)) + .Then(x => ThenTheStatusCodeShouldBe(TooManyRequests)) + .And(x => GivenIWait(1000)) + .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Url, 1)) + .Then(x => ThenTheStatusCodeShouldBe(OK)) + .And(x => ThenTheResponseBodyShouldBe("4")) // total 4 OK responses + .BDDfy(); + } + + private int _count = 0; + private int Count() => ++_count; + private string Url() => $"/ClientRateLimit/?{Count()}"; + + private void WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Func urlDelegate, long times) + { + for (long i = 0; i < times; i++) + { + var url = urlDelegate.Invoke(); + WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(url, 1); + } + } + + [Fact] + [Trait("Feat", "37")] + public void Should_call_middleware_with_white_list_client() + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, null, null, whitelist: new() { "ocelotclient1" }, 3, "3s", 2); // main period is greater than ban one + var configuration = GivenConfigurationWithRateLimitOptions(route); + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(port), "/api/ClientRateLimit")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 4)) + .Then(x => ThenTheStatusCodeShouldBe(OK)) + .BDDfy(); + } + + [Fact] + [Trait("Bug", "1590")] + public void StatusShouldNotBeEqualTo429_PeriodTimespanValueIsGreaterThanPeriod() + { + _counterOne = 0; + + // Bug scenario + const string period = "1s"; + const double periodTimespan = /*30*/3; // but decrease 30 to 3 secs, "no wasting time" life hack + const long limit = 100L; + + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, "/api/ClientRateLimit?count={count}", "/ClientRateLimit/?{count}", new(), + limit, period, periodTimespan); // bug scenario, adapted + var configuration = GivenConfigurationWithRateLimitOptions(route); + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(port), "/api/ClientRateLimit")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + + // main scenario + .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Url, route.RateLimitOptions.Limit)) // 100 times to reach the limit + .Then(x => ThenTheStatusCodeShouldBe(OK)) + .And(x => ThenTheResponseBodyShouldBe(route.RateLimitOptions.Limit.ToString())) // total 100 OK responses + + // extra scenario + .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Url, 1)) // 101st request should fail + .Then(x => ThenTheStatusCodeShouldBe(TooManyRequests)) + .And(x => GivenIWait((int)TimeSpan.FromSeconds(route.RateLimitOptions.PeriodTimespan).TotalMilliseconds)) // in 3 secs PeriodTimespan will elapse + .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Url, 1)) + .Then(x => ThenTheStatusCodeShouldBe(OK)) + .And(x => ThenTheResponseBodyShouldBe("101")) // total 101 OK responses + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, context => + { + _counterOne++; + context.Response.StatusCode = OK; + context.Response.WriteAsync(_counterOne.ToString()); + return Task.CompletedTask; + }); + } + + private FileRoute GivenRoute(int port, string downstream, string upstream, List whitelist, long limit, string period, double periodTimespan) => new() + { + DownstreamPathTemplate = downstream ?? "/api/ClientRateLimit", + DownstreamHostAndPorts = new() + { + new("localhost", port), + }, + DownstreamScheme = Uri.UriSchemeHttp, + UpstreamPathTemplate = upstream ?? "/api/ClientRateLimit", + UpstreamHttpMethod = new() { HttpMethods.Get }, + RequestIdKey = RequestIdKey, + RateLimitOptions = new FileRateLimitRule + { + EnableRateLimiting = true, + ClientWhitelist = whitelist ?? new() { "ocelotclient1" }, + Limit = limit, + Period = period ?? "1s", + PeriodTimespan = periodTimespan, + }, + }; + + private static FileConfiguration GivenConfigurationWithRateLimitOptions(params FileRoute[] routes) + { + var config = GivenConfiguration(routes); + config.GlobalConfiguration = new() + { + RateLimitOptions = new() + { + ClientIdHeader = "ClientId", + DisableRateLimitHeaders = false, + QuotaExceededMessage = "Exceeding!", + RateLimitCounterPrefix = "ABC", + HttpStatusCode = TooManyRequests, // 429 + }, + RequestIdKey = "OcelotClientRequest", + }; + return config; + } +} diff --git a/test/Ocelot.AcceptanceTests/Request/RequestMapperTests.cs b/test/Ocelot.AcceptanceTests/Request/RequestMapperTests.cs new file mode 100644 index 0000000000..ea93d6d8c2 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/Request/RequestMapperTests.cs @@ -0,0 +1,152 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; +using System.Text; + +namespace Ocelot.AcceptanceTests.Request; + +[Trait("PR", "1972")] +public sealed class RequestMapperTests : Steps, IDisposable +{ + private readonly ServiceHandler _serviceHandler; + + public RequestMapperTests() + { + _serviceHandler = new(); + } + + public override void Dispose() + { + _serviceHandler.Dispose(); + base.Dispose(); + } + + [Fact] + public void Should_map_request_without_content() + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(port), "/", HttpStatusCode.OK)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(";;")) + .BDDfy(); + } + + [Fact] + public void Should_map_request_with_content_length() + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, HttpMethods.Post); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(port), "/", HttpStatusCode.OK)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIPostUrlOnTheApiGateway("/", new StringContent("This is some content"))) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("20;;This is some content")) + .BDDfy(); + } + + [Fact] + public void Should_map_request_with_empty_content() + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, HttpMethods.Post); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(port), "/", HttpStatusCode.OK)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIPostUrlOnTheApiGateway("/", new StringContent(""))) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("0;;")) + .BDDfy(); + } + + [Fact] + public void Should_map_request_with_chunked_content() + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, HttpMethods.Post); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(port), "/", HttpStatusCode.OK)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIPostUrlOnTheApiGateway("/", new ChunkedContent("This ", "is some content"))) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(";chunked;This is some content")) + .BDDfy(); + } + + [Fact] + public void Should_map_request_with_empty_chunked_content() + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, HttpMethods.Post); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(port), "/", HttpStatusCode.OK)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIPostUrlOnTheApiGateway("/", new ChunkedContent())) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(";chunked;")) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, HttpStatusCode status) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + var request = context.Request; + var response = context.Response; + response.StatusCode = (int)status; + + await response.WriteAsync(request.ContentLength + ";" + request.Headers.TransferEncoding + ";"); + await request.Body.CopyToAsync(response.Body); + }); + } + + private static FileRoute GivenRoute(int port, string method = null) => new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = Uri.UriSchemeHttp, + DownstreamHostAndPorts = new() + { + new("localhost", port), + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new() { method ?? HttpMethods.Get }, + }; +} + +internal class ChunkedContent : HttpContent +{ + private readonly string[] _chunks; + + public ChunkedContent(params string[] chunks) + { + _chunks = chunks; + } + + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + foreach (var chunk in _chunks) + { + var bytes = Encoding.Default.GetBytes(chunk); + await stream.WriteAsync(bytes, 0, bytes.Length); + } + } + + protected override bool TryComputeLength(out long length) + { + length = -1; + return false; + } +} diff --git a/test/Ocelot.AcceptanceTests/Request/StreamContentTests.cs b/test/Ocelot.AcceptanceTests/Request/StreamContentTests.cs new file mode 100644 index 0000000000..1e97a36d1e --- /dev/null +++ b/test/Ocelot.AcceptanceTests/Request/StreamContentTests.cs @@ -0,0 +1,135 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Ocelot.Configuration.File; +using System.Security.Cryptography; + +namespace Ocelot.AcceptanceTests.Request; + +[Trait("PR", "1972")] +public sealed class StreamContentTests : Steps, IDisposable +{ + private readonly ServiceHandler _serviceHandler; + + public StreamContentTests() + { + _serviceHandler = new ServiceHandler(); + } + + public override void Dispose() + { + _serviceHandler.Dispose(); + base.Dispose(); + } + + [Fact] + public void Should_stream_with_content_length() + { + var contentSize = 1024L * 1024L * 1024L; // 1GB + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, HttpMethods.Post); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(port), "/")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIPostUrlOnTheApiGateway("/", new StreamTestContent(contentSize, false))) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(contentSize + ";;" + contentSize)) + .BDDfy(); + } + + [Fact] + public void Should_stream_with_chunked_content() + { + var contentSize = 1024L * 1024L * 1024L; // 1GB + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, HttpMethods.Post); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(port), "/")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIPostUrlOnTheApiGateway("/", new StreamTestContent(contentSize, true))) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(";chunked;" + contentSize)) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath) + { + static void options(KestrelServerOptions o) + { + o.Limits.MaxRequestBodySize = long.MaxValue; + } + + _serviceHandler.GivenThereIsAServiceRunningOnWithKestrelOptions(baseUrl, basePath, options, async context => + { + var request = context.Request; + var response = context.Response; + + long streamLength = 0; + int readBytes; + var buffer = new byte[8192 - 1]; // Not aligned to sender + + do + { + readBytes = await request.Body.ReadAsync(buffer, 0, buffer.Length); + streamLength += readBytes; + } while (readBytes > 0); + + response.StatusCode = 200; + await response.WriteAsync(request.ContentLength + ";" + request.Headers.TransferEncoding + ";" + streamLength); + }); + } + + private static FileRoute GivenRoute(int port, string method = null) => new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = Uri.UriSchemeHttp, + DownstreamHostAndPorts = new() + { + new("localhost", port), + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new() { method ?? HttpMethods.Get }, + }; +} + +internal class StreamTestContent : HttpContent +{ + private readonly long _size; + private readonly bool _sendChunked; + private readonly byte[] _dataBuffer; + + public StreamTestContent(long size, bool sendChunked) + { + _size = size; + _sendChunked = sendChunked; + _dataBuffer = RandomNumberGenerator.GetBytes(8192); + } + + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + var remaining = _size; + while (remaining > 0) + { + var count = (int)Math.Min(remaining, _dataBuffer.Length); + await stream.WriteAsync(_dataBuffer, 0, count); + remaining -= count; + } + } + + protected override bool TryComputeLength(out long length) + { + if (_sendChunked) + { + length = -1; + return false; + } + else + { + length = _size; + return true; + } + } +} diff --git a/test/Ocelot.AcceptanceTests/Requester/PayloadTooLargeTests.cs b/test/Ocelot.AcceptanceTests/Requester/PayloadTooLargeTests.cs new file mode 100644 index 0000000000..fdb5741ed2 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/Requester/PayloadTooLargeTests.cs @@ -0,0 +1,166 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; +using Ocelot.Middleware; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ocelot.AcceptanceTests.Requester; + +public sealed class PayloadTooLargeTests : Steps, IDisposable +{ + private readonly ServiceHandler _serviceHandler; + private IHost _realServer; + + private const string Payload = + "[{\"_id\":\"6540f8ee7beff536c1304e3a\",\"index\":0,\"guid\":\"349307e2-5b1b-4ea9-8e42-d0d26b35059e\",\"isActive\":true,\"balance\":\"$2,458.86\",\"picture\":\"http://placehold.it/32x32\",\"age\":36,\"eyeColor\":\"blue\",\"name\":\"WalshSloan\",\"gender\":\"male\",\"company\":\"ENOMEN\",\"email\":\"walshsloan@enomen.com\",\"phone\":\"+1(818)463-2479\",\"address\":\"863StoneAvenue,Islandia,NewHampshire,7062\",\"about\":\"Exvelitelitutsintlaborisofficialaborisreprehenderittemporsitminim.Exveniamexetesse.Reprehenderitirurealiquipsuntnostrudcillumaliquipsuntvoluptateessenisivoluptatetemporexercitationsint.Laborumexestipsumincididuntvelit.Idnisiproidenttemporelitnonconsequatestnostrudmollit.\\r\\n\",\"registered\":\"2014-11-13T01:53:09-01:00\",\"latitude\":-1.01137,\"longitude\":160.133312,\"tags\":[\"nisi\",\"eu\",\"anim\",\"ipsum\",\"fugiat\",\"excepteur\",\"culpa\"],\"friends\":[{\"id\":0,\"name\":\"MayNoel\"},{\"id\":1,\"name\":\"RichardsDiaz\"},{\"id\":2,\"name\":\"JannieHarvey\"}],\"greeting\":\"Hello,WalshSloan!Youhave6unreadmessages.\",\"favoriteFruit\":\"banana\"},{\"_id\":\"6540f8ee39e04d0ac854b05d\",\"index\":1,\"guid\":\"0f210e11-94a1-45c7-84a4-c2bfcbe0bbfb\",\"isActive\":false,\"balance\":\"$3,371.91\",\"picture\":\"http://placehold.it/32x32\",\"age\":25,\"eyeColor\":\"green\",\"name\":\"FergusonIngram\",\"gender\":\"male\",\"company\":\"DOGSPA\",\"email\":\"fergusoningram@dogspa.com\",\"phone\":\"+1(804)599-2376\",\"address\":\"130RiverStreet,Bellamy,DistrictOfColumbia,9522\",\"about\":\"Duisvoluptatemollitullamcomollitessedolorvelit.Nonpariaturadipisicingsintdoloranimveniammollitdolorlaborumquisnulla.Ametametametnonlaborevoluptate.Eiusmoddocupidatatveniamirureessequiullamcoincididuntea.\\r\\n\",\"registered\":\"2014-11-01T03:51:36-01:00\",\"latitude\":-57.122954,\"longitude\":-91.22665,\"tags\":[\"nostrud\",\"ipsum\",\"id\",\"cupidatat\",\"consectetur\",\"labore\",\"ullamco\"],\"friends\":[{\"id\":0,\"name\":\"TabithaHuffman\"},{\"id\":1,\"name\":\"LydiaStark\"},{\"id\":2,\"name\":\"FaithStuart\"}],\"greeting\":\"Hello,FergusonIngram!Youhave3unreadmessages.\",\"favoriteFruit\":\"banana\"}]"; + + public PayloadTooLargeTests() + { + _serviceHandler = new ServiceHandler(); + } + + /// + /// Disposes the instance. + /// + /// + /// Dispose pattern is implemented in the base class. + /// + public override void Dispose() + { + _serviceHandler.Dispose(); + _realServer?.Dispose(); + base.Dispose(); + } + + [Fact] + public void Should_throw_payload_too_large_exception_using_kestrel() + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, HttpMethods.Post); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(port))) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningOnKestrelWithCustomBodyMaxSize(1024)) + .When(x => WhenIPostUrlOnTheApiGateway("/", new ByteArrayContent(Encoding.UTF8.GetBytes(Payload)))) + .Then(x => ThenTheStatusCodeShouldBe((int)HttpStatusCode.RequestEntityTooLarge)) + .BDDfy(); + } + + [SkippableFact] + public void Should_throw_payload_too_large_exception_using_http_sys() + { + Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, HttpMethods.Post); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(port))) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningOnHttpSysWithCustomBodyMaxSize(1024)) + .When(x => WhenIPostUrlOnTheApiGateway("/", new ByteArrayContent(Encoding.UTF8.GetBytes(Payload)))) + .Then(x => ThenTheStatusCodeShouldBe((int)HttpStatusCode.RequestEntityTooLarge)) + .BDDfy(); + } + + private static FileRoute GivenRoute(int port, string method = null) => new() + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new() + { + new("localhost", port), + }, + DownstreamScheme = Uri.UriSchemeHttp, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new() {method ?? HttpMethods.Get }, + }; + + private void GivenThereIsAServiceRunningOn(string baseUrl) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, async context => + { + context.Response.StatusCode = (int)HttpStatusCode.OK; + await context.Response.WriteAsync(string.Empty); + }); + } + + private void GivenOcelotIsRunningOnKestrelWithCustomBodyMaxSize(long customBodyMaxSize) + { + _realServer = Host.CreateDefaultBuilder() + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseKestrel() + .ConfigureKestrel((_, options) => + { + options.Limits.MaxRequestBodySize = customBodyMaxSize; + }) + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); + config.AddJsonFile(_ocelotConfigFileName, optional: true, reloadOnChange: false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot(); + }) + .Configure(app => + { + app.UseOcelot().Wait(); + }) + .UseUrls("http://localhost:5001"); + }).Build(); + _realServer.Start(); + + _ocelotClient = new HttpClient + { + BaseAddress = new Uri("http://localhost:5001"), + }; + } + + private void GivenOcelotIsRunningOnHttpSysWithCustomBodyMaxSize(long customBodyMaxSize) + { + _realServer = Host.CreateDefaultBuilder() + .ConfigureWebHostDefaults(webBuilder => + { +#pragma warning disable CA1416 // Validate platform compatibility + webBuilder.UseHttpSys(options => + { + options.MaxRequestBodySize = customBodyMaxSize; + }) +#pragma warning restore CA1416 // Validate platform compatibility + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); + config.AddJsonFile(_ocelotConfigFileName, optional: true, reloadOnChange: false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot(); + }) + .Configure(app => + { + app.UseOcelot().Wait(); + }) + .UseUrls("http://localhost:5001"); + }).Build(); + _realServer.Start(); + + _ocelotClient = new HttpClient + { + BaseAddress = new Uri("http://localhost:5001"), + }; + } +} diff --git a/test/Ocelot.AcceptanceTests/RoutingTests.cs b/test/Ocelot.AcceptanceTests/RoutingTests.cs index e95d3fab82..b881a831f9 100644 --- a/test/Ocelot.AcceptanceTests/RoutingTests.cs +++ b/test/Ocelot.AcceptanceTests/RoutingTests.cs @@ -566,9 +566,9 @@ public void should_return_not_found_when_upstream_url_ends_with_forward_slash_bu } [Theory] - [InlineData("/products", "/products/{productId}", "/products/")] + [InlineData("/products/{productId}", "/products/{productId}", "/products/")] - public void should_return_200_found(string downstreamPathTemplate, string upstreamPathTemplate, string requestURL) + public void should_return_response_200_with_empty_placeholder(string downstreamPathTemplate, string upstreamPathTemplate, string requestURL) { var port = PortFinder.GetRandomPort(); @@ -582,11 +582,7 @@ public void should_return_200_found(string downstreamPathTemplate, string upstre DownstreamScheme = "http", DownstreamHostAndPorts = new List { - new() - { - Host = "localhost", - Port = port, - }, + new("localhost", port), }, UpstreamPathTemplate = upstreamPathTemplate, UpstreamHttpMethod = new List { "Get" }, @@ -594,12 +590,12 @@ public void should_return_200_found(string downstreamPathTemplate, string upstre }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", downstreamPathTemplate, HttpStatusCode.OK, "Hello from Laura")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", requestURL, HttpStatusCode.OK, "Hello from Aly")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway(requestURL)) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .Then(x => ThenTheDownstreamUrlPathShouldBe(downstreamPathTemplate)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Aly")) .BDDfy(); } @@ -897,12 +893,13 @@ public void should_return_response_201_with_simple_url_and_multiple_upstream_htt }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", string.Empty, HttpStatusCode.Created, string.Empty)) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", HttpStatusCode.Created, nameof(HttpStatusCode.Created))) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .And(x => _steps.GivenThePostHasContent("postContent")) .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Created)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Created)) + .And(x => _steps.ThenTheResponseBodyShouldBe(nameof(HttpStatusCode.Created))) .BDDfy(); } @@ -1179,8 +1176,8 @@ private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, Http if (_downstreamPath != basePath) { - context.Response.StatusCode = (int)statusCode; - await context.Response.WriteAsync("downstream path didnt match base path"); + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + await context.Response.WriteAsync("Downstream path didn't match base path"); } else { @@ -1213,7 +1210,7 @@ internal void ThenTheDownstreamUrlQueryStringShouldBe(string expectedQueryString new("localhost", port), }, UpstreamPathTemplate = upstream, - UpstreamHttpMethod = [HttpMethods.Get], + UpstreamHttpMethod = new() { HttpMethods.Get }, }, }, }; diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs similarity index 74% rename from test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs rename to test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs index 79e0dd505d..d25c2075bc 100644 --- a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs @@ -2,595 +2,743 @@ using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Ocelot.Configuration.File; - -namespace Ocelot.AcceptanceTests -{ - public class ServiceDiscoveryTests : IDisposable - { - private readonly Steps _steps; - private readonly List _consulServices; - private int _counterOne; - private int _counterTwo; - private static readonly object SyncLock = new(); - private string _downstreamPath; - private string _receivedToken; - private readonly ServiceHandler _serviceHandler; - - public ServiceDiscoveryTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - _consulServices = new List(); - } - - [Fact] - public void should_use_consul_service_discovery_and_load_balance_request() - { - var consulPort = PortFinder.GetRandomPort(); - var servicePort1 = PortFinder.GetRandomPort(); - var servicePort2 = PortFinder.GetRandomPort(); - var serviceName = "product"; - var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; - var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort1, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - var serviceEntryTwo = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort2, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 50)) - .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) - .BDDfy(); - } - - [Fact] - public void should_handle_request_to_consul_for_downstream_service_and_make_request() - { - var consulPort = PortFinder.GetRandomPort(); - var servicePort = PortFinder.GetRandomPort(); - const string serviceName = "web"; - var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort, - ID = "web_90_0_2_224_8080", - Tags = new[] { "version-v1" }, - }, - }; - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/home", - DownstreamScheme = "http", - UpstreamPathTemplate = "/home", - UpstreamHttpMethod = new List { "Get", "Options" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/home")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes() - { - var consulPort = PortFinder.GetRandomPort(); - const string serviceName = "web"; - var downstreamServicePort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = downstreamServicePort, - ID = "web_90_0_2_224_8080", - Tags = new[] { "version-v1" }, - }, - }; - - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - DownstreamScheme = "http", - HttpHandlerOptions = new FileHttpHandlerOptions - { - AllowAutoRedirect = true, - UseCookieContainer = true, - UseTracing = false, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/something", 200, "Hello from Laura")) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/web/something")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_use_consul_service_discovery_and_load_balance_request_no_re_routes() - { - var consulPort = PortFinder.GetRandomPort(); - var serviceName = "product"; - var serviceOnePort = PortFinder.GetRandomPort(); - var serviceTwoPort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{serviceOnePort}"; - var downstreamServiceTwoUrl = $"http://localhost:{serviceTwoPort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = serviceOnePort, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - var serviceEntryTwo = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = serviceTwoPort, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - DownstreamScheme = "http", - }, - }; - - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes($"/{serviceName}/", 50)) - .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) - .BDDfy(); - } - - [Fact] - public void should_use_token_to_make_request_to_consul() - { - var token = "abctoken"; - var consulPort = PortFinder.GetRandomPort(); - var serviceName = "web"; - var servicePort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort, - ID = "web_90_0_2_224_8080", - Tags = new[] { "version-v1" }, - }, - }; - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/home", - DownstreamScheme = "http", - UpstreamPathTemplate = "/home", - UpstreamHttpMethod = new List { "Get", "Options" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - Token = token, - }, - }, - }; - - this.Given(_ => GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) - .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) - .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(_ => _steps.GivenThereIsAConfiguration(configuration)) - .And(_ => _steps.GivenOcelotIsRunningWithConsul()) - .When(_ => _steps.WhenIGetUrlOnTheApiGateway("/home")) - .Then(_ => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(_ => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(_ => ThenTheTokenIs(token)) - .BDDfy(); - } - - [Fact] - public void should_send_request_to_service_after_it_becomes_available_in_consul() - { - var consulPort = PortFinder.GetRandomPort(); - var serviceName = "product"; - var servicePort1 = PortFinder.GetRandomPort(); - var servicePort2 = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; - var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort1, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - var serviceEntryTwo = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort2, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) - .And(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) - .And(x => WhenIRemoveAService(serviceEntryTwo)) - .And(x => GivenIResetCounters()) - .And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) - .And(x => ThenOnlyOneServiceHasBeenCalled()) - .And(x => WhenIAddAServiceBackIn(serviceEntryTwo)) - .And(x => GivenIResetCounters()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) - .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) - .BDDfy(); - } - - [Fact] - public void should_handle_request_to_poll_consul_for_downstream_service_and_make_request() - { - var consulPort = PortFinder.GetRandomPort(); - const string serviceName = "web"; - var downstreamServicePort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = downstreamServicePort, - ID = $"web_90_0_2_224_{downstreamServicePort}", - Tags = new[] { "version-v1" }, - }, - }; - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/home", - DownstreamScheme = "http", - UpstreamPathTemplate = "/home", - UpstreamHttpMethod = new List { "Get", "Options" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - Type = "PollConsul", - PollingInterval = 0, - Namespace = string.Empty, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk("/home")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); +using System.Text.RegularExpressions; + +namespace Ocelot.AcceptanceTests.ServiceDiscovery +{ + public class ConsulServiceDiscoveryTests : IDisposable + { + private readonly Steps _steps; + private readonly List _consulServices; + private int _counterOne; + private int _counterTwo; + private int _counterConsul; + private static readonly object SyncLock = new(); + private string _downstreamPath; + private string _receivedToken; + private readonly ServiceHandler _serviceHandler; + private readonly ServiceHandler _serviceHandler2; + private readonly ServiceHandler _consulHandler; + + public ConsulServiceDiscoveryTests() + { + _serviceHandler = new ServiceHandler(); + _serviceHandler2 = new ServiceHandler(); + _consulHandler = new ServiceHandler(); + _steps = new Steps(); + _consulServices = new List(); + } + + [Fact] + public void should_use_consul_service_discovery_and_load_balance_request() + { + var consulPort = PortFinder.GetRandomPort(); + var servicePort1 = PortFinder.GetRandomPort(); + var servicePort2 = PortFinder.GetRandomPort(); + var serviceName = "product"; + var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; + var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = servicePort1, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + var serviceEntryTwo = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = servicePort2, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = serviceName, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithConsul()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 50)) + .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) + .BDDfy(); + } + + [Fact] + public void should_handle_request_to_consul_for_downstream_service_and_make_request() + { + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); + const string serviceName = "web"; + var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = servicePort, + ID = "web_90_0_2_224_8080", + Tags = new[] { "version-v1" }, + }, + }; + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/home", + DownstreamScheme = "http", + UpstreamPathTemplate = "/home", + UpstreamHttpMethod = new List { "Get", "Options" }, + ServiceName = serviceName, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithConsul()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/home")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes() + { + var consulPort = PortFinder.GetRandomPort(); + const string serviceName = "web"; + var downstreamServicePort = PortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = downstreamServicePort, + ID = "web_90_0_2_224_8080", + Tags = new[] { "version-v1" }, + }, + }; + + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + DownstreamScheme = "http", + HttpHandlerOptions = new FileHttpHandlerOptions + { + AllowAutoRedirect = true, + UseCookieContainer = true, + UseTracing = false, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/something", 200, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithConsul()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/web/something")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_use_consul_service_discovery_and_load_balance_request_no_re_routes() + { + var consulPort = PortFinder.GetRandomPort(); + var serviceName = "product"; + var serviceOnePort = PortFinder.GetRandomPort(); + var serviceTwoPort = PortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{serviceOnePort}"; + var downstreamServiceTwoUrl = $"http://localhost:{serviceTwoPort}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = serviceOnePort, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + var serviceEntryTwo = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = serviceTwoPort, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + DownstreamScheme = "http", + }, + }; + + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithConsul()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes($"/{serviceName}/", 50)) + .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) + .BDDfy(); + } + + [Fact] + public void should_use_token_to_make_request_to_consul() + { + var token = "abctoken"; + var consulPort = PortFinder.GetRandomPort(); + var serviceName = "web"; + var servicePort = PortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = servicePort, + ID = "web_90_0_2_224_8080", + Tags = new[] { "version-v1" }, + }, + }; + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/home", + DownstreamScheme = "http", + UpstreamPathTemplate = "/home", + UpstreamHttpMethod = new List { "Get", "Options" }, + ServiceName = serviceName, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + Token = token, + }, + }, + }; + + this.Given(_ => GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) + .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(_ => _steps.GivenThereIsAConfiguration(configuration)) + .And(_ => _steps.GivenOcelotIsRunningWithConsul()) + .When(_ => _steps.WhenIGetUrlOnTheApiGateway("/home")) + .Then(_ => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(_ => ThenTheTokenIs(token)) + .BDDfy(); + } + + [Fact] + public void should_send_request_to_service_after_it_becomes_available_in_consul() + { + var consulPort = PortFinder.GetRandomPort(); + var serviceName = "product"; + var servicePort1 = PortFinder.GetRandomPort(); + var servicePort2 = PortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; + var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = servicePort1, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + var serviceEntryTwo = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = servicePort2, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = serviceName, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithConsul()) + .And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) + .And(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) + .And(x => WhenIRemoveAService(serviceEntryTwo)) + .And(x => GivenIResetCounters()) + .And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) + .And(x => ThenOnlyOneServiceHasBeenCalled()) + .And(x => WhenIAddAServiceBackIn(serviceEntryTwo)) + .And(x => GivenIResetCounters()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) + .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) + .BDDfy(); + } + + [Fact] + public void should_handle_request_to_poll_consul_for_downstream_service_and_make_request() + { + var consulPort = PortFinder.GetRandomPort(); + const string serviceName = "web"; + var downstreamServicePort = PortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = downstreamServicePort, + ID = $"web_90_0_2_224_{downstreamServicePort}", + Tags = new[] { "version-v1" }, + }, + }; + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/home", + DownstreamScheme = "http", + UpstreamPathTemplate = "/home", + UpstreamHttpMethod = new List { "Get", "Options" }, + ServiceName = serviceName, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + Type = "PollConsul", + PollingInterval = 0, + Namespace = string.Empty, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithConsul()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk("/home")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Theory] + [Trait("PR", "1944")] + [Trait("Issues", "849 1496")] + [InlineData("LeastConnection")] + [InlineData("RoundRobin")] + [InlineData("NoLoadBalancer")] + [InlineData("CookieStickySessions")] + public void Should_use_consul_service_discovery_based_on_upstream_host(string loadBalancerType) + { + // Simulate two DIFFERENT downstream services (e.g. product services for US and EU markets) + // with different ServiceNames (e.g. product-us and product-eu), + // UpstreamHost is used to determine which ServiceName to use when making a request to Consul (e.g. Host: us-shop goes to product-us) + var consulPort = PortFinder.GetRandomPort(); + var servicePortUS = PortFinder.GetRandomPort(); + var servicePortEU = PortFinder.GetRandomPort(); + var serviceNameUS = "product-us"; + var serviceNameEU = "product-eu"; + var downstreamServiceUrlUS = $"http://localhost:{servicePortUS}"; + var downstreamServiceUrlEU = $"http://localhost:{servicePortEU}"; + var upstreamHostUS = "us-shop"; + var upstreamHostEU = "eu-shop"; + var publicUrlUS = $"http://{upstreamHostUS}"; + var publicUrlEU = $"http://{upstreamHostEU}"; + var responseBodyUS = "Phone chargers with US plug"; + var responseBodyEU = "Phone chargers with EU plug"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryUS = new ServiceEntry + { + Service = new AgentService + { + Service = serviceNameUS, + Address = "localhost", + Port = servicePortUS, + ID = Guid.NewGuid().ToString(), + Tags = new string[] { "US" }, + }, + }; + var serviceEntryEU = new ServiceEntry + { + Service = new AgentService + { + Service = serviceNameEU, + Address = "localhost", + Port = servicePortEU, + ID = Guid.NewGuid().ToString(), + Tags = new string[] { "EU" }, + }, + }; + + var configuration = new FileConfiguration + { + Routes = new() + { + new() + { + DownstreamPathTemplate = "/products", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new() { "Get" }, + UpstreamHost = upstreamHostUS, + ServiceName = serviceNameUS, + LoadBalancerOptions = new() { Type = loadBalancerType }, + }, + new() + { + DownstreamPathTemplate = "/products", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new() {"Get" }, + UpstreamHost = upstreamHostEU, + ServiceName = serviceNameEU, + LoadBalancerOptions = new() { Type = loadBalancerType }, + }, + }, + GlobalConfiguration = new() + { + ServiceDiscoveryProvider = new() + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + // Ocelot request for http://us-shop/ should find 'product-us' in Consul, call /products and return "Phone chargers with US plug" + // Ocelot request for http://eu-shop/ should find 'product-eu' in Consul, call /products and return "Phone chargers with EU plug" + this.Given(x => x._serviceHandler.GivenThereIsAServiceRunningOn(downstreamServiceUrlUS, "/products", MapGet("/products", responseBodyUS))) + .And(x => x._serviceHandler2.GivenThereIsAServiceRunningOn(downstreamServiceUrlEU, "/products", MapGet("/products", responseBodyEU))) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryUS, serviceEntryEU)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithConsul(publicUrlUS, publicUrlEU)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlUS), "When I get US shop for the first time") + .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(1)) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyUS)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlEU), "When I get EU shop for the first time") + .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(2)) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyEU)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlUS), "When I get US shop again") + .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(3)) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyUS)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlEU), "When I get EU shop again") + .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(4)) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyEU)) + .BDDfy(); } private void ThenTheTokenIs(string token) { _receivedToken.ShouldBe(token); - } - - private void WhenIAddAServiceBackIn(ServiceEntry serviceEntryTwo) - { - _consulServices.Add(serviceEntryTwo); - } - - private void ThenOnlyOneServiceHasBeenCalled() - { - _counterOne.ShouldBe(10); - _counterTwo.ShouldBe(0); - } - - private void WhenIRemoveAService(ServiceEntry serviceEntryTwo) - { - _consulServices.Remove(serviceEntryTwo); - } - - private void GivenIResetCounters() - { - _counterOne = 0; - _counterTwo = 0; - } - - private void ThenBothServicesCalledRealisticAmountOfTimes(int bottom, int top) - { - _counterOne.ShouldBeInRange(bottom, top); - _counterOne.ShouldBeInRange(bottom, top); - } - - private void ThenTheTwoServicesShouldHaveBeenCalledTimes(int expected) - { - var total = _counterOne + _counterTwo; - total.ShouldBe(expected); - } - - private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) - { - foreach (var serviceEntry in serviceEntries) - { - _consulServices.Add(serviceEntry); - } - } - - private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") - { - if (context.Request.Headers.TryGetValue("X-Consul-Token", out var values)) - { - _receivedToken = values.First(); - } - - var json = JsonConvert.SerializeObject(_consulServices); - context.Response.Headers.Append("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - }); - } - - private void GivenProductServiceOneIsRunning(string url, int statusCode) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - try - { - string response; - lock (SyncLock) - { - _counterOne++; - response = _counterOne.ToString(); - } - - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(response); - } - catch (Exception exception) - { - await context.Response.WriteAsync(exception.StackTrace); - } - }); - } - - private void GivenProductServiceTwoIsRunning(string url, int statusCode) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - try - { - string response; - lock (SyncLock) - { - _counterTwo++; - response = _counterTwo.ToString(); - } - - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(response); - } - catch (Exception exception) - { - await context.Response.WriteAsync(exception.StackTrace); - } - }); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - - if (_downstreamPath != basePath) - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - else - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - } - }); - } - - public void Dispose() - { - _serviceHandler?.Dispose(); - _steps.Dispose(); - } - } -} + } + + private void WhenIAddAServiceBackIn(ServiceEntry serviceEntryTwo) + { + _consulServices.Add(serviceEntryTwo); + } + + private void ThenOnlyOneServiceHasBeenCalled() + { + _counterOne.ShouldBe(10); + _counterTwo.ShouldBe(0); + } + + private void WhenIRemoveAService(ServiceEntry serviceEntryTwo) + { + _consulServices.Remove(serviceEntryTwo); + } + + private void GivenIResetCounters() + { + _counterOne = 0; + _counterTwo = 0; + _counterConsul = 0; + } + + private void ThenBothServicesCalledRealisticAmountOfTimes(int bottom, int top) + { + _counterOne.ShouldBeInRange(bottom, top); + _counterOne.ShouldBeInRange(bottom, top); + } + + private void ThenTheTwoServicesShouldHaveBeenCalledTimes(int expected) + { + var total = _counterOne + _counterTwo; + total.ShouldBe(expected); + } + + private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) + { + foreach (var serviceEntry in serviceEntries) + { + _consulServices.Add(serviceEntry); + } + } + + private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url) + { + _consulHandler.GivenThereIsAServiceRunningOn(url, async context => + { + if (context.Request.Headers.TryGetValue("X-Consul-Token", out var values)) + { + _receivedToken = values.First(); + } + + // Parse the request path to get the service name + var pathMatch = Regex.Match(context.Request.Path.Value, "/v1/health/service/(?[^/]+)"); + if (pathMatch.Success) + { + _counterConsul++; + + // Use the parsed service name to filter the registered Consul services + var serviceName = pathMatch.Groups["serviceName"].Value; + var services = _consulServices.Where(x => x.Service.Service == serviceName).ToList(); + var json = JsonConvert.SerializeObject(services); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + } + + private void ThenConsulShouldHaveBeenCalledTimes(int expected) + { + _counterConsul.ShouldBe(expected); + } + + private void GivenProductServiceOneIsRunning(string url, int statusCode) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + try + { + string response; + lock (SyncLock) + { + _counterOne++; + response = _counterOne.ToString(); + } + + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(response); + } + catch (Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); + } + + private void GivenProductServiceTwoIsRunning(string url, int statusCode) + { + _serviceHandler2.GivenThereIsAServiceRunningOn(url, async context => + { + try + { + string response; + lock (SyncLock) + { + _counterTwo++; + response = _counterTwo.ToString(); + } + + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(response); + } + catch (Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPath != basePath) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + } + + private RequestDelegate MapGet(string path, string responseBody) => async context => + { + var downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + if (downstreamPath == path) + { + context.Response.StatusCode = 200; + await context.Response.WriteAsync(responseBody); + } + else + { + context.Response.StatusCode = 404; + await context.Response.WriteAsync("Not Found"); + } + }; + + public void Dispose() + { + _serviceHandler?.Dispose(); + _serviceHandler2?.Dispose(); + _consulHandler?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/EurekaServiceDiscoveryTests.cs similarity index 96% rename from test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs rename to test/Ocelot.AcceptanceTests/ServiceDiscovery/EurekaServiceDiscoveryTests.cs index 1b7c1e99f6..a12c206419 100644 --- a/test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/EurekaServiceDiscoveryTests.cs @@ -1,281 +1,282 @@ using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Ocelot.Configuration.File; +using Ocelot.LoadBalancer.LoadBalancers; using Steeltoe.Common.Discovery; - -namespace Ocelot.AcceptanceTests -{ - public class EurekaServiceDiscoveryTests : IDisposable - { - private readonly Steps _steps; - private readonly List _eurekaInstances; - private readonly ServiceHandler _serviceHandler; - - public EurekaServiceDiscoveryTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - _eurekaInstances = new List(); - } - + +namespace Ocelot.AcceptanceTests.ServiceDiscovery +{ + public class EurekaServiceDiscoveryTests : IDisposable + { + private readonly Steps _steps; + private readonly List _eurekaInstances; + private readonly ServiceHandler _serviceHandler; + + public EurekaServiceDiscoveryTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + _eurekaInstances = new List(); + } + [Theory] [InlineData(true)] - [InlineData(false)] - public void should_use_eureka_service_discovery_and_make_request(bool dotnetRunningInContainer) - { - Environment.SetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER", dotnetRunningInContainer.ToString()); - var eurekaPort = 8761; - var serviceName = "product"; + [InlineData(false)] + public void should_use_eureka_service_discovery_and_make_request(bool dotnetRunningInContainer) + { + Environment.SetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER", dotnetRunningInContainer.ToString()); + var eurekaPort = 8761; + var serviceName = "product"; var downstreamServicePort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; - var fakeEurekaServiceDiscoveryUrl = $"http://localhost:{eurekaPort}"; - - var instanceOne = new FakeEurekaService(serviceName, "localhost", downstreamServicePort, false, - new Uri($"http://localhost:{downstreamServicePort}"), new Dictionary()); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Type = "Eureka", - }, - }, - }; - - this.Given(x => x.GivenEurekaProductServiceOneIsRunning(downstreamServiceOneUrl)) - .And(x => x.GivenThereIsAFakeEurekaServiceDiscoveryProvider(fakeEurekaServiceDiscoveryUrl, serviceName)) - .And(x => x.GivenTheServicesAreRegisteredWithEureka(instanceOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithEureka()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(_ => _steps.ThenTheResponseBodyShouldBe(nameof(EurekaServiceDiscoveryTests))) - .BDDfy(); - } - - private void GivenTheServicesAreRegisteredWithEureka(params IServiceInstance[] serviceInstances) - { - foreach (var instance in serviceInstances) - { - _eurekaInstances.Add(instance); - } - } - - private void GivenThereIsAFakeEurekaServiceDiscoveryProvider(string url, string serviceName) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - if (context.Request.Path.Value == "/eureka/apps/") - { - var apps = new List(); - - foreach (var serviceInstance in _eurekaInstances) - { - var a = new Application - { - name = serviceName, - instance = new List - { - new() - { - instanceId = $"{serviceInstance.Host}:{serviceInstance}", - hostName = serviceInstance.Host, - app = serviceName, - ipAddr = "127.0.0.1", - status = "UP", - overriddenstatus = "UNKNOWN", - port = new Port {value = serviceInstance.Port, enabled = "true"}, - securePort = new SecurePort {value = serviceInstance.Port, enabled = "true"}, - countryId = 1, - dataCenterInfo = new DataCenterInfo {value = "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", name = "MyOwn"}, - leaseInfo = new LeaseInfo - { - renewalIntervalInSecs = 30, - durationInSecs = 90, - registrationTimestamp = 1457714988223, - lastRenewalTimestamp= 1457716158319, - evictionTimestamp = 0, - serviceUpTimestamp = 1457714988223, - }, - metadata = new Metadata - { - value = "java.util.Collections$EmptyMap", - }, - homePageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", - statusPageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", - healthCheckUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", - vipAddress = serviceName, - isCoordinatingDiscoveryServer = "false", - lastUpdatedTimestamp = "1457714988223", - lastDirtyTimestamp = "1457714988172", - actionType = "ADDED", - }, - }, - }; - - apps.Add(a); - } - - var applications = new EurekaApplications - { - applications = new Applications - { - application = apps, - apps__hashcode = "UP_1_", - versions__delta = "1", - }, - }; - - var json = JsonConvert.SerializeObject(applications); - context.Response.Headers.Append("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - }); - } - - private void GivenEurekaProductServiceOneIsRunning(string url) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - try - { - context.Response.StatusCode = 200; - await context.Response.WriteAsync(nameof(EurekaServiceDiscoveryTests)); - } - catch (Exception exception) - { - await context.Response.WriteAsync(exception.StackTrace); - } - }); - } - - public void Dispose() + var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; + var fakeEurekaServiceDiscoveryUrl = $"http://localhost:{eurekaPort}"; + + var instanceOne = new FakeEurekaService(serviceName, "localhost", downstreamServicePort, false, + new Uri($"http://localhost:{downstreamServicePort}"), new Dictionary()); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = serviceName, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = nameof(LeastConnection) }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Type = "Eureka", + }, + }, + }; + + this.Given(x => x.GivenEurekaProductServiceOneIsRunning(downstreamServiceOneUrl)) + .And(x => x.GivenThereIsAFakeEurekaServiceDiscoveryProvider(fakeEurekaServiceDiscoveryUrl, serviceName)) + .And(x => x.GivenTheServicesAreRegisteredWithEureka(instanceOne)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithEureka()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => _steps.ThenTheResponseBodyShouldBe(nameof(EurekaServiceDiscoveryTests))) + .BDDfy(); + } + + private void GivenTheServicesAreRegisteredWithEureka(params IServiceInstance[] serviceInstances) + { + foreach (var instance in serviceInstances) + { + _eurekaInstances.Add(instance); + } + } + + private void GivenThereIsAFakeEurekaServiceDiscoveryProvider(string url, string serviceName) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + if (context.Request.Path.Value == "/eureka/apps/") + { + var apps = new List(); + + foreach (var serviceInstance in _eurekaInstances) + { + var a = new Application + { + name = serviceName, + instance = new List + { + new() + { + instanceId = $"{serviceInstance.Host}:{serviceInstance}", + hostName = serviceInstance.Host, + app = serviceName, + ipAddr = "127.0.0.1", + status = "UP", + overriddenstatus = "UNKNOWN", + port = new Port {value = serviceInstance.Port, enabled = "true"}, + securePort = new SecurePort {value = serviceInstance.Port, enabled = "true"}, + countryId = 1, + dataCenterInfo = new DataCenterInfo {value = "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", name = "MyOwn"}, + leaseInfo = new LeaseInfo + { + renewalIntervalInSecs = 30, + durationInSecs = 90, + registrationTimestamp = 1457714988223, + lastRenewalTimestamp= 1457716158319, + evictionTimestamp = 0, + serviceUpTimestamp = 1457714988223, + }, + metadata = new Metadata + { + value = "java.util.Collections$EmptyMap", + }, + homePageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", + statusPageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", + healthCheckUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", + vipAddress = serviceName, + isCoordinatingDiscoveryServer = "false", + lastUpdatedTimestamp = "1457714988223", + lastDirtyTimestamp = "1457714988172", + actionType = "ADDED", + }, + }, + }; + + apps.Add(a); + } + + var applications = new EurekaApplications + { + applications = new Applications + { + application = apps, + apps__hashcode = "UP_1_", + versions__delta = "1", + }, + }; + + var json = JsonConvert.SerializeObject(applications); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + } + + private void GivenEurekaProductServiceOneIsRunning(string url) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + try + { + context.Response.StatusCode = 200; + await context.Response.WriteAsync(nameof(EurekaServiceDiscoveryTests)); + } + catch (Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); + _steps.Dispose(); + } + } + + public class FakeEurekaService : IServiceInstance + { + public FakeEurekaService(string serviceId, string host, int port, bool isSecure, Uri uri, IDictionary metadata) { - _serviceHandler?.Dispose(); - _steps.Dispose(); - } - } - - public class FakeEurekaService : IServiceInstance - { - public FakeEurekaService(string serviceId, string host, int port, bool isSecure, Uri uri, IDictionary metadata) - { - ServiceId = serviceId; - Host = host; - Port = port; - IsSecure = isSecure; - Uri = uri; - Metadata = metadata; - } - - public string ServiceId { get; } - public string Host { get; } - public int Port { get; } - public bool IsSecure { get; } - public Uri Uri { get; } - public IDictionary Metadata { get; } - } - - public class Port - { - [JsonProperty("$")] - public int value { get; set; } - - [JsonProperty("@enabled")] - public string enabled { get; set; } - } - - public class SecurePort - { - [JsonProperty("$")] - public int value { get; set; } - - [JsonProperty("@enabled")] - public string enabled { get; set; } - } - - public class DataCenterInfo - { - [JsonProperty("@class")] - public string value { get; set; } - - public string name { get; set; } - } - - public class LeaseInfo - { - public int renewalIntervalInSecs { get; set; } - - public int durationInSecs { get; set; } - - public long registrationTimestamp { get; set; } - - public long lastRenewalTimestamp { get; set; } - - public int evictionTimestamp { get; set; } - - public long serviceUpTimestamp { get; set; } - } - - public class Metadata - { - [JsonProperty("@class")] - public string value { get; set; } - } - - public class Instance - { - public string instanceId { get; set; } - public string hostName { get; set; } - public string app { get; set; } - public string ipAddr { get; set; } - public string status { get; set; } - public string overriddenstatus { get; set; } - public Port port { get; set; } - public SecurePort securePort { get; set; } - public int countryId { get; set; } - public DataCenterInfo dataCenterInfo { get; set; } - public LeaseInfo leaseInfo { get; set; } - public Metadata metadata { get; set; } - public string homePageUrl { get; set; } - public string statusPageUrl { get; set; } - public string healthCheckUrl { get; set; } - public string vipAddress { get; set; } - public string isCoordinatingDiscoveryServer { get; set; } - public string lastUpdatedTimestamp { get; set; } - public string lastDirtyTimestamp { get; set; } - public string actionType { get; set; } - } - - public class Application - { - public string name { get; set; } - public List instance { get; set; } - } - - public class Applications - { - public string versions__delta { get; set; } - public string apps__hashcode { get; set; } - public List application { get; set; } - } - - public class EurekaApplications - { - public Applications applications { get; set; } - } -} + ServiceId = serviceId; + Host = host; + Port = port; + IsSecure = isSecure; + Uri = uri; + Metadata = metadata; + } + + public string ServiceId { get; } + public string Host { get; } + public int Port { get; } + public bool IsSecure { get; } + public Uri Uri { get; } + public IDictionary Metadata { get; } + } + + public class Port + { + [JsonProperty("$")] + public int value { get; set; } + + [JsonProperty("@enabled")] + public string enabled { get; set; } + } + + public class SecurePort + { + [JsonProperty("$")] + public int value { get; set; } + + [JsonProperty("@enabled")] + public string enabled { get; set; } + } + + public class DataCenterInfo + { + [JsonProperty("@class")] + public string value { get; set; } + + public string name { get; set; } + } + + public class LeaseInfo + { + public int renewalIntervalInSecs { get; set; } + + public int durationInSecs { get; set; } + + public long registrationTimestamp { get; set; } + + public long lastRenewalTimestamp { get; set; } + + public int evictionTimestamp { get; set; } + + public long serviceUpTimestamp { get; set; } + } + + public class Metadata + { + [JsonProperty("@class")] + public string value { get; set; } + } + + public class Instance + { + public string instanceId { get; set; } + public string hostName { get; set; } + public string app { get; set; } + public string ipAddr { get; set; } + public string status { get; set; } + public string overriddenstatus { get; set; } + public Port port { get; set; } + public SecurePort securePort { get; set; } + public int countryId { get; set; } + public DataCenterInfo dataCenterInfo { get; set; } + public LeaseInfo leaseInfo { get; set; } + public Metadata metadata { get; set; } + public string homePageUrl { get; set; } + public string statusPageUrl { get; set; } + public string healthCheckUrl { get; set; } + public string vipAddress { get; set; } + public string isCoordinatingDiscoveryServer { get; set; } + public string lastUpdatedTimestamp { get; set; } + public string lastDirtyTimestamp { get; set; } + public string actionType { get; set; } + } + + public class Application + { + public string name { get; set; } + public List instance { get; set; } + } + + public class Applications + { + public string versions__delta { get; set; } + public string apps__hashcode { get; set; } + public List application { get; set; } + } + + public class EurekaApplications + { + public Applications applications { get; set; } + } +} diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscovery/KubernetesServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/KubernetesServiceDiscoveryTests.cs new file mode 100644 index 0000000000..5ca22da6eb --- /dev/null +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/KubernetesServiceDiscoveryTests.cs @@ -0,0 +1,212 @@ +using KubeClient; +using KubeClient.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Newtonsoft.Json; +using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Provider.Kubernetes; +using System.Runtime.CompilerServices; + +namespace Ocelot.AcceptanceTests.ServiceDiscovery; + +public sealed class KubernetesServiceDiscoveryTests : Steps, IDisposable +{ + private readonly string _kubernetesUrl; + private readonly IKubeApiClient _clientFactory; + private readonly ServiceHandler _serviceHandler; + private readonly ServiceHandler _kubernetesHandler; + private string _receivedToken; + + public KubernetesServiceDiscoveryTests() + { + _kubernetesUrl = DownstreamUrl(PortFinder.GetRandomPort()); //5567 + var option = new KubeClientOptions + { + ApiEndPoint = new Uri(_kubernetesUrl), + AccessToken = "txpc696iUhbVoudg164r93CxDTrKRVWG", + AuthStrategy = KubeAuthStrategy.BearerToken, + AllowInsecure = true, + }; + _clientFactory = KubeApiClient.Create(option); + _serviceHandler = new ServiceHandler(); + _kubernetesHandler = new ServiceHandler(); + } + + public override void Dispose() + { + _serviceHandler.Dispose(); + _kubernetesHandler.Dispose(); + base.Dispose(); + } + + [Fact] + public void ShouldReturnServicesFromK8s() + { + const string namespaces = nameof(KubernetesServiceDiscoveryTests); + const string serviceName = nameof(ShouldReturnServicesFromK8s); + var servicePort = PortFinder.GetRandomPort(); + var downstreamUrl = DownstreamUrl(servicePort); + var downstream = new Uri(downstreamUrl); + var subsetV1 = new EndpointSubsetV1(); + subsetV1.Addresses.Add(new() + { + Ip = Dns.GetHostAddresses(downstream.Host).Select(x => x.ToString()).First(a => a.Contains('.')), + Hostname = downstream.Host, + }); + subsetV1.Ports.Add(new() + { + Name = downstream.Scheme, + Port = servicePort, + }); + var endpoints = GivenEndpoints(subsetV1); + var route = GivenRouteWithServiceName(namespaces); + var configuration = GivenKubeConfiguration(namespaces, route); + var downstreamResponse = serviceName; + this.Given(x => GivenK8sProductServiceOneIsRunning(downstreamUrl, downstreamResponse)) + .And(x => GivenThereIsAFakeKubernetesProvider(serviceName, namespaces, endpoints)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => x.GivenOcelotIsRunningWithKubernetes()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => ThenTheResponseBodyShouldBe(downstreamResponse)) + .And(_ => ThenTheTokenIs("Bearer txpc696iUhbVoudg164r93CxDTrKRVWG")) + .BDDfy(); + } + + [Theory] + [Trait("Feat", "1967")] + [InlineData("", HttpStatusCode.BadGateway)] + [InlineData("http", HttpStatusCode.OK)] + public void ShouldReturnServicesByPortNameAsDownstreamScheme(string downstreamScheme, HttpStatusCode statusCode) + { + const string serviceName = "example-web"; + const string namespaces = "default"; + var servicePort = PortFinder.GetRandomPort(); + var downstreamUrl = DownstreamUrl(servicePort); + var downstream = new Uri(downstreamUrl); + + var subsetV1 = new EndpointSubsetV1(); + subsetV1.Addresses.Add(new() + { + Ip = Dns.GetHostAddresses(downstream.Host).Select(x => x.ToString()).First(a => a.Contains('.')), + Hostname = downstream.Host, + }); + subsetV1.Ports.Add(new() + { + Name = "https", // This service instance is offline -> BadGateway + Port = 443, + }); + subsetV1.Ports.Add(new() + { + Name = downstream.Scheme, // http, should be real scheme + Port = downstream.Port, // not 80, should be real port + }); + var endpoints = GivenEndpoints(subsetV1); + + var route = GivenRouteWithServiceName(namespaces); + route.DownstreamPathTemplate = "/{url}"; + route.DownstreamScheme = downstreamScheme; // !!! Warning !!! Select port by name as scheme + route.UpstreamPathTemplate = "/api/example/{url}"; + route.ServiceName = serviceName; // "example-web" + var configuration = GivenKubeConfiguration(namespaces, route); + + this.Given(x => GivenK8sProductServiceOneIsRunning(downstreamUrl, nameof(ShouldReturnServicesByPortNameAsDownstreamScheme))) + .And(x => GivenThereIsAFakeKubernetesProvider(serviceName, namespaces, endpoints)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => x.GivenOcelotIsRunningWithKubernetes()) + .When(x => WhenIGetUrlOnTheApiGateway("/api/example/1")) + .Then(x => ThenTheStatusCodeShouldBe(statusCode)) + .And(_ => ThenTheResponseBodyShouldBe(downstreamScheme == "http" + ? nameof(ShouldReturnServicesByPortNameAsDownstreamScheme) : string.Empty)) + .And(_ => ThenTheTokenIs("Bearer txpc696iUhbVoudg164r93CxDTrKRVWG")) + .BDDfy(); + } + + private void ThenTheTokenIs(string token) + { + _receivedToken.ShouldBe(token); + } + + private EndpointsV1 GivenEndpoints(EndpointSubsetV1 subset, [CallerMemberName] string serviceName = "") + { + var e = new EndpointsV1() + { + Kind = "endpoint", + ApiVersion = "1.0", + Metadata = new() + { + Name = serviceName, + Namespace = nameof(KubernetesServiceDiscoveryTests), + }, + }; + e.Subsets.Add(subset); + return e; + } + + private FileRoute GivenRouteWithServiceName(string serviceNamespace, [CallerMemberName] string serviceName = null) => new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = Uri.UriSchemeHttp, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new() { HttpMethods.Get }, + ServiceName = serviceName, + ServiceNamespace = serviceNamespace, + LoadBalancerOptions = new() { Type = nameof(LeastConnection) }, + }; + + private FileConfiguration GivenKubeConfiguration(string serviceNamespace, params FileRoute[] routes) + { + var u = new Uri(_kubernetesUrl); + var configuration = GivenConfiguration(routes); + configuration.GlobalConfiguration.ServiceDiscoveryProvider = new() + { + Scheme = u.Scheme, + Host = u.Host, + Port = u.Port, + Type = nameof(Kube), + PollingInterval = 0, + Namespace = serviceNamespace, + }; + return configuration; + } + + private void GivenThereIsAFakeKubernetesProvider(string serviceName, string namespaces, EndpointsV1 endpoints) + => _kubernetesHandler.GivenThereIsAServiceRunningOn(_kubernetesUrl, async context => + { + if (context.Request.Path.Value == $"/api/v1/namespaces/{namespaces}/endpoints/{serviceName}") + { + if (context.Request.Headers.TryGetValue("Authorization", out var values)) + { + _receivedToken = values.First(); + } + + var json = JsonConvert.SerializeObject(endpoints); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + + private void GivenOcelotIsRunningWithKubernetes() + => GivenOcelotIsRunningWithServices(s => + { + s.AddOcelot().AddKubernetes(); + s.RemoveAll().AddSingleton(_clientFactory); + }); + + private void GivenK8sProductServiceOneIsRunning(string url, string response) + => _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + try + { + context.Response.StatusCode = (int)HttpStatusCode.OK; + await context.Response.WriteAsync(response ?? nameof(HttpStatusCode.OK)); + } + catch (Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); +} diff --git a/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ServiceFabricTests.cs similarity index 85% rename from test/Ocelot.AcceptanceTests/ServiceFabricTests.cs rename to test/Ocelot.AcceptanceTests/ServiceDiscovery/ServiceFabricTests.cs index 87d511a332..739b19a7db 100644 --- a/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ServiceFabricTests.cs @@ -1,203 +1,209 @@ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; - -namespace Ocelot.AcceptanceTests -{ - public class ServiceFabricTests : IDisposable - { - private readonly Steps _steps; - private string _downstreamPath; - private readonly ServiceHandler _serviceHandler; - - public ServiceFabricTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } - - [Fact] - public void should_fix_issue_555() - { - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/{everything}", - DownstreamScheme = "http", - UpstreamPathTemplate = "/{everything}", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = "OcelotServiceApplication/OcelotApplicationService", - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "localhost", - Port = port, - Type = "ServiceFabric", - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/OcelotServiceApplication/OcelotApplicationService/a", 200, "Hello from Laura", "b=c")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/a?b=c")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_support_service_fabric_naming_and_dns_service_stateless_and_guest() - { - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/values", - DownstreamScheme = "http", - UpstreamPathTemplate = "/EquipmentInterfaces", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = "OcelotServiceApplication/OcelotApplicationService", - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "localhost", - Port = port, - Type = "ServiceFabric", - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/OcelotServiceApplication/OcelotApplicationService/api/values", 200, "Hello from Laura", "test=best")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/EquipmentInterfaces?test=best")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_support_service_fabric_naming_and_dns_service_statefull_and_actors() - { - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/values", - DownstreamScheme = "http", - UpstreamPathTemplate = "/EquipmentInterfaces", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = "OcelotServiceApplication/OcelotApplicationService", - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "localhost", - Port = port, - Type = "ServiceFabric", - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/OcelotServiceApplication/OcelotApplicationService/api/values", 200, "Hello from Laura", "PartitionKind=test&PartitionKey=1")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/EquipmentInterfaces?PartitionKind=test&PartitionKey=1")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_support_placeholder_in_service_fabric_service_name() - { - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/values", - DownstreamScheme = "http", - UpstreamPathTemplate = "/api/{version}/values", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = "Service_{version}/Api", - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "localhost", - Port = port, - Type = "ServiceFabric", - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/Service_1.0/Api/values", 200, "Hello from Laura", "test=best")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api/1.0/values?test=best")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody, string expectedQueryString) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - - if (_downstreamPath != basePath) - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - else - { - if (context.Request.QueryString.Value.Contains(expectedQueryString)) - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - } - else - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - } - }); - } - - public void Dispose() - { - _serviceHandler?.Dispose(); - _steps.Dispose(); - } - } -} + +namespace Ocelot.AcceptanceTests.ServiceDiscovery +{ + public class ServiceFabricTests : IDisposable + { + private readonly Steps _steps; + private string _downstreamPath; + private readonly ServiceHandler _serviceHandler; + + public ServiceFabricTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + } + + [Fact] + [Trait("PR", "570")] + [Trait("Bug", "555")] + public void should_fix_issue_555() + { + var port = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/{everything}", + DownstreamScheme = "http", + UpstreamPathTemplate = "/{everything}", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = "OcelotServiceApplication/OcelotApplicationService", + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Host = "localhost", + Port = port, + Type = "ServiceFabric", + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/OcelotServiceApplication/OcelotApplicationService/a", 200, "Hello from Laura", "b=c")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/a?b=c")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_support_service_fabric_naming_and_dns_service_stateless_and_guest() + { + var port = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/values", + DownstreamScheme = "http", + UpstreamPathTemplate = "/EquipmentInterfaces", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = "OcelotServiceApplication/OcelotApplicationService", + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Host = "localhost", + Port = port, + Type = "ServiceFabric", + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/OcelotServiceApplication/OcelotApplicationService/api/values", 200, "Hello from Laura", "test=best")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/EquipmentInterfaces?test=best")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_support_service_fabric_naming_and_dns_service_statefull_and_actors() + { + var port = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/values", + DownstreamScheme = "http", + UpstreamPathTemplate = "/EquipmentInterfaces", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = "OcelotServiceApplication/OcelotApplicationService", + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Host = "localhost", + Port = port, + Type = "ServiceFabric", + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/OcelotServiceApplication/OcelotApplicationService/api/values", 200, "Hello from Laura", "PartitionKind=test&PartitionKey=1")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/EquipmentInterfaces?PartitionKind=test&PartitionKey=1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Theory] + [Trait("PR", "722")] + [Trait("Feat", "721")] + [InlineData("/api/{version}/values", "/values", "Service_{version}/Api", "/Service_1.0/Api/values", "/api/1.0/values?test=best", "test=best")] + [InlineData("/api/{version}/{all}", "/{all}", "Service_{version}/Api", "/Service_1.0/Api/products", "/api/1.0/products?test=the-best-from-Aly", "test=the-best-from-Aly")] + public void should_support_placeholder_in_service_fabric_service_name(string upstream, string downstream, string serviceName, string downstreamUrl, string url, string query) + { + var port = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new() + { + new() + { + DownstreamPathTemplate = downstream, + DownstreamScheme = Uri.UriSchemeHttp, + UpstreamPathTemplate = upstream, + UpstreamHttpMethod = new() { HttpMethods.Get }, + ServiceName = serviceName, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Host = "localhost", + Port = port, + Type = "ServiceFabric", + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", downstreamUrl, 200, "Hello from Felix Boers", query)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway(url)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Felix Boers")) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody, string expectedQueryString) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPath != basePath) + { + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + if (context.Request.QueryString.Value.Contains(expectedQueryString)) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + else + { + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + } + }); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ServiceHandler.cs b/test/Ocelot.AcceptanceTests/ServiceHandler.cs index 9ba0d0fe46..c2e10a819f 100644 --- a/test/Ocelot.AcceptanceTests/ServiceHandler.cs +++ b/test/Ocelot.AcceptanceTests/ServiceHandler.cs @@ -5,7 +5,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using System.Security.Authentication; namespace Ocelot.AcceptanceTests { @@ -46,18 +45,12 @@ public void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, Reque _builder.Start(); } - public void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, RequestDelegate del, int port, HttpProtocols protocols) + public void GivenThereIsAServiceRunningOnWithKestrelOptions(string baseUrl, string basePath, Action options, RequestDelegate del) { _builder = new WebHostBuilder() .UseUrls(baseUrl) .UseKestrel() - .ConfigureKestrel(serverOptions => - { - serverOptions.Listen(IPAddress.Loopback, port, listenOptions => - { - listenOptions.Protocols = protocols; - }); - }) + .ConfigureKestrel(options ?? WithDefaultKestrelServerOptions) // ! .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .Configure(app => @@ -70,32 +63,8 @@ public void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, Reque _builder.Start(); } - public void GivenThereIsAServiceRunningOnUsingHttps(string baseUrl, string basePath, RequestDelegate del, int port, HttpProtocols protocols) + internal void WithDefaultKestrelServerOptions(KestrelServerOptions options) { - _builder = new WebHostBuilder() - .UseUrls(baseUrl) - .UseKestrel() - .ConfigureKestrel(serverOptions => - { - serverOptions.Listen(IPAddress.Loopback, port, listenOptions => - { - listenOptions.UseHttps("mycert.pfx", "password", options => - { - options.SslProtocols = SslProtocols.Tls12; - }); - listenOptions.Protocols = protocols; - }); - }) - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .Configure(app => - { - app.UsePathBase(basePath); - app.Run(del); - }) - .Build(); - - _builder.Start(); } public void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, string fileName, string password, int port, RequestDelegate del) diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index e6265b93c7..0d5505fcdd 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Ocelot.AcceptanceTests.Caching; @@ -18,7 +19,6 @@ using Ocelot.LoadBalancer.LoadBalancers; using Ocelot.Logging; using Ocelot.Middleware; -using Ocelot.Multiplexer; using Ocelot.Provider.Consul; using Ocelot.Provider.Eureka; using Ocelot.Provider.Polly; @@ -46,6 +46,7 @@ public class Steps : IDisposable private BearerToken _token; public string RequestIdKey = "OcRequestId"; private readonly Random _random; + protected readonly Guid _testId; protected readonly string _ocelotConfigFileName; protected IWebHostBuilder _webHostBuilder; private WebHostBuilder _ocelotBuilder; @@ -55,9 +56,19 @@ public class Steps : IDisposable public Steps() { _random = new Random(); - _ocelotConfigFileName = $"{Guid.NewGuid():N}-ocelot.json"; + _testId = Guid.NewGuid(); + _ocelotConfigFileName = $"{_testId:N}-{ConfigurationBuilderExtensions.PrimaryConfigFile}"; } + protected string TestID { get => _testId.ToString("N"); } + + protected static string DownstreamUrl(int port) => $"{Uri.UriSchemeHttp}://localhost:{port}"; + + protected static FileConfiguration GivenConfiguration(params FileRoute[] routes) => new() + { + Routes = new(routes), + }; + public async Task ThenConfigShouldBe(FileConfiguration fileConfig) { var internalConfigCreator = _ocelotServer.Host.Services.GetService(); @@ -161,21 +172,25 @@ public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) File.WriteAllText(_ocelotConfigFileName, jsonConfiguration); } - private void DeleteOcelotConfig() + protected virtual void DeleteOcelotConfig(params string[] files) { - if (!File.Exists(_ocelotConfigFileName)) + var allFiles = files.Append(_ocelotConfigFileName); + foreach (var file in allFiles) { - return; - } + if (!File.Exists(file)) + { + continue; + } - try - { - File.Delete(_ocelotConfigFileName); - } - catch (Exception e) - { - Console.WriteLine(e); - } + try + { + File.Delete(file); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } } public void ThenTheResponseBodyHeaderIs(string key, string value) @@ -186,24 +201,7 @@ public void ThenTheResponseBodyHeaderIs(string key, string value) public void GivenOcelotIsRunningReloadingConfig(bool shouldReload) { - _webHostBuilder = new WebHostBuilder(); - - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", true, false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); - config.AddJsonFile(_ocelotConfigFileName, false, shouldReload); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => { s.AddOcelot(); }) - .Configure(app => { app.UseOcelot().Wait(); }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); + StartOcelot((_, config) => config.AddJsonFile(_ocelotConfigFileName, false, shouldReload)); } public void GivenIHaveAChangeToken() @@ -215,24 +213,29 @@ public void GivenIHaveAChangeToken() /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step. /// public void GivenOcelotIsRunning() + { + StartOcelot((_, config) => config.AddJsonFile(_ocelotConfigFileName, false, false)); + } + + protected void StartOcelot(Action configureAddOcelot) { _webHostBuilder = new WebHostBuilder(); _webHostBuilder .ConfigureAppConfiguration((hostingContext, config) => { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); var env = hostingContext.HostingEnvironment; + config.SetBasePath(env.ContentRootPath); config.AddJsonFile("appsettings.json", true, false) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); - config.AddJsonFile(_ocelotConfigFileName, false, false); + configureAddOcelot.Invoke(hostingContext, config); // config.AddOcelot(...); config.AddEnvironmentVariables(); }) - .ConfigureServices(s => { s.AddOcelot(); }) - .Configure(app => { app.UseOcelot().Wait(); }); + .ConfigureServices(WithAddOcelot) + .Configure(WithUseOcelot) + .UseEnvironment(nameof(AcceptanceTests)); _ocelotServer = new TestServer(_webHostBuilder); - _ocelotClient = _ocelotServer.CreateClient(); } @@ -241,13 +244,10 @@ public void GivenOcelotIsRunning() /// /// The type. /// The delegate object to load balancer factory. - public void GivenOcelotIsRunningWithCustomLoadBalancer( - Func loadBalancerFactoryFunc) + public void GivenOcelotIsRunningWithCustomLoadBalancer(Func loadBalancerFactoryFunc) where T : ILoadBalancer { - _webHostBuilder = new WebHostBuilder(); - - _webHostBuilder + _webHostBuilder = new WebHostBuilder() .ConfigureAppConfiguration((hostingContext, config) => { config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); @@ -265,14 +265,18 @@ public void GivenOcelotIsRunningWithCustomLoadBalancer( .Configure(app => { app.UseOcelot().Wait(); }); _ocelotServer = new TestServer(_webHostBuilder); - _ocelotClient = _ocelotServer.CreateClient(); } - public void GivenOcelotIsRunningWithConsul() + public void GivenOcelotIsRunningWithConsul(params string[] urlsToListenOn) { _webHostBuilder = new WebHostBuilder(); + if (urlsToListenOn?.Length > 0) + { + _webHostBuilder.UseUrls(urlsToListenOn); + } + _webHostBuilder .ConfigureAppConfiguration((hostingContext, config) => { @@ -502,36 +506,6 @@ public void GivenOcelotIsRunningWithSpecificHandlersRegisteredInDi() _ocelotClient = _ocelotServer.CreateClient(); } - public void GivenOcelotIsRunningWithSpecificAggregatorsRegisteredInDi() - where TAggregator : class, IDefinedAggregator - where TDependency : class - { - _webHostBuilder = new WebHostBuilder(); - - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", true, false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); - config.AddJsonFile(_ocelotConfigFileName, true, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(_webHostBuilder); - s.AddSingleton(); - s.AddOcelot() - .AddSingletonDefinedAggregator(); - }) - .Configure(a => { a.UseOcelot().Wait(); }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } - public void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi() where TOne : DelegatingHandler where TWo : DelegatingHandler @@ -667,11 +641,14 @@ public void ThenTheReasonPhraseIs(string expected) } public void GivenOcelotIsRunningWithServices(Action configureServices) + => GivenOcelotIsRunningWithServices(configureServices, null); + + public void GivenOcelotIsRunningWithServices(Action configureServices, Action configureApp) { _webHostBuilder = new WebHostBuilder() .ConfigureAppConfiguration(WithBasicConfiguration) .ConfigureServices(configureServices ?? WithAddOcelot) - .Configure(WithUseOcelot); + .Configure(configureApp ?? WithUseOcelot); _ocelotServer = new TestServer(_webHostBuilder); _ocelotClient = _ocelotServer.CreateClient(); } @@ -802,60 +779,10 @@ public void GivenOcelotIsRunningWithMinimumLogLevel(Logger logger, string appset } public void GivenOcelotIsRunningWithEureka() - { - _webHostBuilder = new WebHostBuilder(); - - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", true, false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() - .AddEureka(); - }) - .Configure(app => { app.UseOcelot().Wait(); }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } + => GivenOcelotIsRunningWithServices(s => s.AddOcelot().AddEureka()); - public void GivenOcelotIsRunningWithPolly() - { - _webHostBuilder = new WebHostBuilder(); - - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", true, false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() - .AddPolly(); - }) - .Configure(app => - { - app.UseOcelot() - .Wait(); - }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } + public void GivenOcelotIsRunningWithPolly() => GivenOcelotIsRunningWithServices(WithPolly); + public static void WithPolly(IServiceCollection services) => services.AddOcelot().AddPolly(); public void WhenIGetUrlOnTheApiGateway(string url) { @@ -867,6 +794,29 @@ public void WhenIGetUrlOnTheApiGatewayAndDontWait(string url) _ocelotClient.GetAsync(url); } + public void WhenIGetUrlWithBodyOnTheApiGateway(string url, string body) + { + var request = new HttpRequestMessage(HttpMethod.Get, url) + { + Content = new StringContent(body), + }; + _response = _ocelotClient.SendAsync(request).Result; + } + + public void WhenIGetUrlWithFormOnTheApiGateway(string url, string name, IEnumerable> values) + { + var content = new MultipartFormDataContent(); + var dataContent = new FormUrlEncodedContent(values); + content.Add(dataContent, name); + content.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data"); + + var request = new HttpRequestMessage(HttpMethod.Get, url) + { + Content = content, + }; + _response = _ocelotClient.SendAsync(request).Result; + } + public void WhenICancelTheRequest() { _ocelotClient.CancelPendingRequests(); diff --git a/test/Ocelot.AcceptanceTests/TestConfiguration.cs b/test/Ocelot.AcceptanceTests/TestConfiguration.cs deleted file mode 100644 index 95e5fe9b7e..0000000000 --- a/test/Ocelot.AcceptanceTests/TestConfiguration.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Ocelot.AcceptanceTests -{ - public static class TestConfiguration - { - public static string ConfigurationPath => Path.Combine(AppContext.BaseDirectory, "ocelot.json"); - } -} diff --git a/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj b/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj index bcaf49e127..288d790195 100644 --- a/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj +++ b/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj @@ -13,9 +13,9 @@ false false false - ..\..\codeanalysis.ruleset True - 1591 + ..\..\codeanalysis.ruleset + $(NoWarn);CS0618;CS1591 diff --git a/test/Ocelot.Benchmarks/PayloadBenchmarks.cs b/test/Ocelot.Benchmarks/PayloadBenchmarks.cs index 7547ad9212..fe776f0eaf 100644 --- a/test/Ocelot.Benchmarks/PayloadBenchmarks.cs +++ b/test/Ocelot.Benchmarks/PayloadBenchmarks.cs @@ -38,20 +38,20 @@ public void SetUp() { var configuration = new FileConfiguration { - Routes = - [ + Routes = new() + { new FileRoute { DownstreamPathTemplate = "/", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort("localhost", 51879), - ], + }, DownstreamScheme = "http", UpstreamPathTemplate = "/", - UpstreamHttpMethod =["Post"], + UpstreamHttpMethod = new() { "Post" }, }, - ], + }, }; GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 201); @@ -113,12 +113,12 @@ private static object[] GeneratePayload(int size, string directory, string fileN { var filePath = Path.Combine(directory, fileName); var generateDummy = isJson ? (Func) GenerateDummyJsonFile : GenerateDummyDatFile; - return - [ + return new object[] + { generateDummy(size, filePath), fileName, isJson, - ]; + }; } /// diff --git a/test/Ocelot.Benchmarks/ResponseBenchmarks.cs b/test/Ocelot.Benchmarks/ResponseBenchmarks.cs index f68ecb254c..a7c1d85b7b 100644 --- a/test/Ocelot.Benchmarks/ResponseBenchmarks.cs +++ b/test/Ocelot.Benchmarks/ResponseBenchmarks.cs @@ -41,20 +41,20 @@ public void SetUp() { var configuration = new FileConfiguration { - Routes = - [ + Routes = new() + { new FileRoute { DownstreamPathTemplate = "/", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort("localhost", 51879), - ], - DownstreamScheme = "http", + }, + DownstreamScheme = Uri.UriSchemeHttp, UpstreamPathTemplate = "/", - UpstreamHttpMethod =["GET"], + UpstreamHttpMethod = new() { HttpMethods.Get }, }, - ], + }, }; GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 201); @@ -115,12 +115,12 @@ private static object[] GeneratePayload(int size, string directory, string fileN { var filePath = Path.Combine(directory, fileName); var generateDummy = isJson ? (Func)GenerateDummyJsonFile : GenerateDummyDatFile; - return - [ + return new object[] + { generateDummy(size, filePath), fileName, isJson, - ]; + }; } /// diff --git a/test/Ocelot.IntegrationTests/AdministrationTests.cs b/test/Ocelot.IntegrationTests/AdministrationTests.cs index 3a57d0d9dc..87ff03fd1c 100644 --- a/test/Ocelot.IntegrationTests/AdministrationTests.cs +++ b/test/Ocelot.IntegrationTests/AdministrationTests.cs @@ -278,7 +278,7 @@ public void Should_get_file_configuration_edit_and_post_updated_version() DownstreamScheme = "https", DownstreamPathTemplate = "/blooper/{productId}", UpstreamHttpMethod = new List { "post" }, - UpstreamPathTemplate = "/test", + UpstreamPathTemplate = "/test/{productId}", }, }, }; @@ -882,7 +882,7 @@ private void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) private void ThenTheResultHaveMultiLineIndentedJson() { const string indent = " "; - const int total = 46, skip = 1; + const int total = 52, skip = 1; var lines = _response.Content.ReadAsStringAsync().Result.Split(Environment.NewLine); lines.Length.ShouldBe(total); lines.First().ShouldNotStartWith(indent); diff --git a/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj b/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj index 996603c557..857f2c0b5e 100644 --- a/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj +++ b/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj @@ -14,9 +14,9 @@ false false false - ..\..\codeanalysis.ruleset True - 1591 + ..\..\codeanalysis.ruleset + $(NoWarn);CS0618;CS1591 diff --git a/samples/Docker/Dockerfile b/test/Ocelot.ManualTest/Dockerfile similarity index 98% rename from samples/Docker/Dockerfile rename to test/Ocelot.ManualTest/Dockerfile index dad54b20f3..31b4580b33 100644 --- a/samples/Docker/Dockerfile +++ b/test/Ocelot.ManualTest/Dockerfile @@ -1,47 +1,47 @@ -#This is the base image used for any ran images -FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-stretch-slim AS base -WORKDIR /app -EXPOSE 80 - -#This image is used to build the source for the runnable app -#It can also be used to run other CLI commands on the project, such as packing/deploying nuget packages. Some examples: -#Run tests: docker build --target builder -t ocelot-build . && docker run ocelot-build test --logger:trx;LogFileName=results.trx -#Run benchmarks: docker build --target builder --build-arg build_configuration=Release -t ocelot-build . && docker run ocelot-build run -c Release --project test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj -FROM mcr.microsoft.com/dotnet/core/sdk:2.2-stretch AS builder -WORKDIR /build -#First we add only the project files so that we can cache nuget packages with dotnet restore -COPY Ocelot.sln Ocelot.sln -COPY src/Ocelot/Ocelot.csproj src/Ocelot/Ocelot.csproj -COPY src/Ocelot.Administration/Ocelot.Administration.csproj src/Ocelot.Administration/Ocelot.Administration.csproj -COPY src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj -COPY src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj -COPY src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj -COPY src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj -COPY src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj -COPY src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj -COPY test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj -COPY test/Ocelot.ManualTest/Ocelot.ManualTest.csproj test/Ocelot.ManualTest/Ocelot.ManualTest.csproj -COPY test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj -COPY test/Ocelot.UnitTests/Ocelot.UnitTests.csproj test/Ocelot.UnitTests/Ocelot.UnitTests.csproj -COPY test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj - -RUN dotnet restore -#Now we add the rest of the source and run a complete build... --no-restore is used because nuget should be resolved at this point -COPY codeanalysis.ruleset codeanalysis.ruleset -COPY src src -COPY test test -ARG build_configuration=Debug -RUN dotnet build --no-restore -c ${build_configuration} -ENTRYPOINT ["dotnet"] - -#This is just for holding the published manual tests... -FROM builder AS manual-test-publish -ARG build_configuration=Debug -RUN dotnet publish --no-build -c ${build_configuration} -o /app test/Ocelot.ManualTest - -#Run manual tests! This is the default run option. -#docker build -t ocelot-manual-test . && docker run --net host ocelot-manual-test -FROM base AS manual-test -ENV ASPNETCORE_ENVIRONMENT=Development -COPY --from=manual-test-publish /app . -ENTRYPOINT ["dotnet", "Ocelot.ManualTest.dll"] +#This is the base image used for any ran images +FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-stretch-slim AS base +WORKDIR /app +EXPOSE 80 + +#This image is used to build the source for the runnable app +#It can also be used to run other CLI commands on the project, such as packing/deploying nuget packages. Some examples: +#Run tests: docker build --target builder -t ocelot-build . && docker run ocelot-build test --logger:trx;LogFileName=results.trx +#Run benchmarks: docker build --target builder --build-arg build_configuration=Release -t ocelot-build . && docker run ocelot-build run -c Release --project test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj +FROM mcr.microsoft.com/dotnet/core/sdk:2.2-stretch AS builder +WORKDIR /build +#First we add only the project files so that we can cache nuget packages with dotnet restore +COPY Ocelot.sln Ocelot.sln +COPY src/Ocelot/Ocelot.csproj src/Ocelot/Ocelot.csproj +COPY src/Ocelot.Administration/Ocelot.Administration.csproj src/Ocelot.Administration/Ocelot.Administration.csproj +COPY src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj +COPY src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj +COPY src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj +COPY src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj +COPY src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj +COPY src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj +COPY test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj +COPY test/Ocelot.ManualTest/Ocelot.ManualTest.csproj test/Ocelot.ManualTest/Ocelot.ManualTest.csproj +COPY test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj +COPY test/Ocelot.UnitTests/Ocelot.UnitTests.csproj test/Ocelot.UnitTests/Ocelot.UnitTests.csproj +COPY test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj + +RUN dotnet restore +#Now we add the rest of the source and run a complete build... --no-restore is used because nuget should be resolved at this point +COPY codeanalysis.ruleset codeanalysis.ruleset +COPY src src +COPY test test +ARG build_configuration=Debug +RUN dotnet build --no-restore -c ${build_configuration} +ENTRYPOINT ["dotnet"] + +#This is just for holding the published manual tests... +FROM builder AS manual-test-publish +ARG build_configuration=Debug +RUN dotnet publish --no-build -c ${build_configuration} -o /app test/Ocelot.ManualTest + +#Run manual tests! This is the default run option. +#docker build -t ocelot-manual-test . && docker run --net host ocelot-manual-test +FROM base AS manual-test +ENV ASPNETCORE_ENVIRONMENT=Development +COPY --from=manual-test-publish /app . +ENTRYPOINT ["dotnet", "Ocelot.ManualTest.dll"] diff --git a/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj b/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj index 81aabb2f1d..919f6b913c 100644 --- a/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj +++ b/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj @@ -9,9 +9,9 @@ Exe Ocelot.ManualTest win-x64;osx-x64 - ..\..\codeanalysis.ruleset True - 1591 + ..\..\codeanalysis.ruleset + $(NoWarn);CS0618;CS1591 diff --git a/samples/Docker-Compose/docker-compose.yaml b/test/Ocelot.ManualTest/docker-compose.yaml similarity index 95% rename from samples/Docker-Compose/docker-compose.yaml rename to test/Ocelot.ManualTest/docker-compose.yaml index 5236202fde..8313684412 100644 --- a/samples/Docker-Compose/docker-compose.yaml +++ b/test/Ocelot.ManualTest/docker-compose.yaml @@ -1,24 +1,24 @@ -version: "3.4" -services: - - tests: - build: - context: . - target: builder - volumes: - - type: bind - source: . - target: /results - command: test --logger:trx -r /results - - benchmarks: - build: - context: . - target: builder - args: - build_configuration: Release - command: run -c Release --project test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj 0 1 2 3 4 - - manual-test: - build: . - ports: [ "5000:80" ] +version: "3.4" +services: + + tests: + build: + context: . + target: builder + volumes: + - type: bind + source: . + target: /results + command: test --logger:trx -r /results + + benchmarks: + build: + context: . + target: builder + args: + build_configuration: Release + command: run -c Release --project test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj 0 1 2 3 4 + + manual-test: + build: . + ports: [ "5000:80" ] diff --git a/test/Ocelot.UnitTests/Administration/OcelotAdministrationBuilderTests.cs b/test/Ocelot.UnitTests/Administration/OcelotAdministrationBuilderTests.cs index 838c6f2396..15c827662b 100644 --- a/test/Ocelot.UnitTests/Administration/OcelotAdministrationBuilderTests.cs +++ b/test/Ocelot.UnitTests/Administration/OcelotAdministrationBuilderTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.Administration { - public class OcelotAdministrationBuilderTests + public class OcelotAdministrationBuilderTests : UnitTest { private readonly IServiceCollection _services; private IServiceProvider _serviceProvider; @@ -22,7 +22,7 @@ public OcelotAdministrationBuilderTests() _services = new ServiceCollection(); _services.AddSingleton(GetHostingEnvironment()); _services.AddSingleton(_configRoot); - } + } private static IWebHostEnvironment GetHostingEnvironment() { diff --git a/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs b/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs index 2201722eb5..0bd9eb6037 100644 --- a/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs @@ -12,7 +12,7 @@ namespace Ocelot.UnitTests.Authentication { - public class AuthenticationMiddlewareTests + public class AuthenticationMiddlewareTests : UnitTest { private readonly Mock _authentication; private readonly Mock _factory; @@ -121,7 +121,7 @@ public void Should_provide_backward_compatibility_if_route_has_several_options_a { var options = new AuthenticationOptions(null, "Test", - [string.Empty, "Fail", "Test"] + new string[] { string.Empty, "Fail", "Test" } ); var methods = new List { "Get" }; this.Given(x => GivenTheDownStreamRouteIs(new DownstreamRouteBuilder() @@ -143,7 +143,7 @@ public void Should_provide_backward_compatibility_if_route_has_several_options_a public void Should_not_call_next_middleware_and_return_no_result_if_all_multiple_keys_were_failed() { var options = new AuthenticationOptions(null, null, - [string.Empty, "Fail", "Fail", "UnknownScheme"] + new string[] { string.Empty, "Fail", "Fail", "UnknownScheme" } ); var methods = new List { "Get" }; diff --git a/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs b/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs index 045257e326..717de3daf7 100644 --- a/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.Authorization { - public class AuthorizationMiddlewareTests + public class AuthorizationMiddlewareTests : UnitTest { private readonly Mock _authService; private readonly Mock _authScopesService; diff --git a/test/Ocelot.UnitTests/Authorization/ClaimsAuthorizerTests.cs b/test/Ocelot.UnitTests/Authorization/ClaimsAuthorizerTests.cs index 45865a456d..6033d0a7a6 100644 --- a/test/Ocelot.UnitTests/Authorization/ClaimsAuthorizerTests.cs +++ b/test/Ocelot.UnitTests/Authorization/ClaimsAuthorizerTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.Authorization { - public class ClaimsAuthorizerTests + public class ClaimsAuthorizerTests : UnitTest { private readonly ClaimsAuthorizer _claimsAuthorizer; private ClaimsPrincipal _claimsPrincipal; diff --git a/test/Ocelot.UnitTests/Cache/RegionCreatorTests.cs b/test/Ocelot.UnitTests/Cache/CacheOptionsCreatorTests.cs similarity index 73% rename from test/Ocelot.UnitTests/Cache/RegionCreatorTests.cs rename to test/Ocelot.UnitTests/Cache/CacheOptionsCreatorTests.cs index 841ff5166b..f3584e7140 100644 --- a/test/Ocelot.UnitTests/Cache/RegionCreatorTests.cs +++ b/test/Ocelot.UnitTests/Cache/CacheOptionsCreatorTests.cs @@ -1,59 +1,60 @@ -using Ocelot.Cache; +using Ocelot.Configuration; +using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; - -namespace Ocelot.UnitTests.Cache -{ - public class RegionCreatorTests - { - private string _result; - private FileRoute _route; - - [Fact] - public void should_create_region() - { - var route = new FileRoute - { - UpstreamHttpMethod = new List { "Get" }, - UpstreamPathTemplate = "/testdummy", - }; - - this.Given(_ => GivenTheRoute(route)) - .When(_ => WhenICreateTheRegion()) - .Then(_ => ThenTheRegionIs("Gettestdummy")) - .BDDfy(); - } - - [Fact] - public void should_use_region() - { - var route = new FileRoute - { - FileCacheOptions = new FileCacheOptions - { - Region = "region", - }, - }; - - this.Given(_ => GivenTheRoute(route)) - .When(_ => WhenICreateTheRegion()) - .Then(_ => ThenTheRegionIs("region")) - .BDDfy(); - } - - private void GivenTheRoute(FileRoute route) - { - _route = route; - } - - private void WhenICreateTheRegion() - { - var regionCreator = new RegionCreator(); - _result = regionCreator.Create(_route); - } - - private void ThenTheRegionIs(string expected) - { - _result.ShouldBe(expected); - } - } -} + +namespace Ocelot.UnitTests.Cache +{ + public class CacheOptionsCreatorTests : UnitTest + { + private CacheOptions _cacheOptions; + private FileRoute _route; + + [Fact] + public void should_create_region() + { + var route = new FileRoute + { + UpstreamHttpMethod = new List { "Get" }, + UpstreamPathTemplate = "/testdummy", + }; + + this.Given(_ => GivenTheRoute(route)) + .When(_ => WhenICreateTheRegion()) + .Then(_ => ThenTheRegionIs("Gettestdummy")) + .BDDfy(); + } + + [Fact] + public void should_use_region() + { + var route = new FileRoute + { + FileCacheOptions = new FileCacheOptions + { + Region = "region", + }, + }; + + this.Given(_ => GivenTheRoute(route)) + .When(_ => WhenICreateTheRegion()) + .Then(_ => ThenTheRegionIs("region")) + .BDDfy(); + } + + private void GivenTheRoute(FileRoute route) + { + _route = route; + } + + private void WhenICreateTheRegion() + { + var cacheOptionsCreator = new CacheOptionsCreator(); + _cacheOptions = cacheOptionsCreator.Create(_route.FileCacheOptions, new FileGlobalConfiguration(), _route.UpstreamPathTemplate, _route.UpstreamHttpMethod); + } + + private void ThenTheRegionIs(string expected) + { + _cacheOptions.Region.ShouldBe(expected); + } + } +} diff --git a/test/Ocelot.UnitTests/Cache/DefaultCacheKeyGeneratorTests.cs b/test/Ocelot.UnitTests/Cache/DefaultCacheKeyGeneratorTests.cs index a81ec7d072..50c7de6c9a 100644 --- a/test/Ocelot.UnitTests/Cache/DefaultCacheKeyGeneratorTests.cs +++ b/test/Ocelot.UnitTests/Cache/DefaultCacheKeyGeneratorTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.Cache; -public sealed class DefaultCacheKeyGeneratorTests : IDisposable +public sealed class DefaultCacheKeyGeneratorTests : UnitTest, IDisposable { private readonly ICacheKeyGenerator _cacheKeyGenerator; private readonly HttpRequestMessage _request; @@ -59,7 +59,7 @@ public void should_generate_cache_key_without_request_content() [Fact] public void should_generate_cache_key_with_cache_options_header() { - CacheOptions options = new CacheOptions(100, "region", headerName); + CacheOptions options = new CacheOptions(100, "region", headerName, false); var cachekey = MD5Helper.GenerateMd5($"{verb}-{url}-{header}"); this.Given(x => x.GivenDownstreamRoute(options)) diff --git a/test/Ocelot.UnitTests/Cache/AspMemoryCacheTests.cs b/test/Ocelot.UnitTests/Cache/DefaultMemoryCacheTests.cs similarity index 90% rename from test/Ocelot.UnitTests/Cache/AspMemoryCacheTests.cs rename to test/Ocelot.UnitTests/Cache/DefaultMemoryCacheTests.cs index 573c076ce8..e5d14dbe4d 100644 --- a/test/Ocelot.UnitTests/Cache/AspMemoryCacheTests.cs +++ b/test/Ocelot.UnitTests/Cache/DefaultMemoryCacheTests.cs @@ -3,13 +3,13 @@ namespace Ocelot.UnitTests.Cache { - public class AspMemoryCacheTests + public class DefaultMemoryCacheTests : UnitTest { - private readonly AspMemoryCache _cache; + private readonly DefaultMemoryCache _cache; - public AspMemoryCacheTests() + public DefaultMemoryCacheTests() { - _cache = new AspMemoryCache(new MemoryCache(new MemoryCacheOptions())); + _cache = new DefaultMemoryCache(new MemoryCache(new MemoryCacheOptions())); } [Fact] diff --git a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs index bb7e829a91..ab00e76752 100644 --- a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Cache { - public class OutputCacheMiddlewareTests + public class OutputCacheMiddlewareTests : UnitTest { private readonly Mock> _cache; private readonly Mock _loggerFactory; @@ -106,7 +106,7 @@ private void GivenTheDownstreamRouteIs() var route = new RouteBuilder() .WithDownstreamRoute(new DownstreamRouteBuilder() .WithIsCached(true) - .WithCacheOptions(new CacheOptions(100, "kanken", null)) + .WithCacheOptions(new CacheOptions(100, "kanken", null, false)) .WithUpstreamHttpMethod(new List { "Get" }) .Build()) .WithUpstreamHttpMethod(new List { "Get" }) diff --git a/test/Ocelot.UnitTests/CacheManager/OcelotBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/CacheManager/OcelotBuilderExtensionsTests.cs index 9d980aa23a..410c73b648 100644 --- a/test/Ocelot.UnitTests/CacheManager/OcelotBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/CacheManager/OcelotBuilderExtensionsTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.CacheManager { - public class OcelotBuilderExtensionsTests + public class OcelotBuilderExtensionsTests : UnitTest { private readonly IServiceCollection _services; private readonly IConfiguration _configRoot; diff --git a/test/Ocelot.UnitTests/CacheManager/OcelotCacheManagerCache.cs b/test/Ocelot.UnitTests/CacheManager/OcelotCacheManagerCache.cs index 82ffc5bf9d..a2a6620294 100644 --- a/test/Ocelot.UnitTests/CacheManager/OcelotCacheManagerCache.cs +++ b/test/Ocelot.UnitTests/CacheManager/OcelotCacheManagerCache.cs @@ -3,7 +3,7 @@ namespace Ocelot.UnitTests.CacheManager { - public class OcelotCacheManagerCache + public class OcelotCacheManagerCache : UnitTest { private readonly OcelotCacheManagerCache _ocelotOcelotCacheManager; private readonly Mock> _mockCacheManager; diff --git a/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs b/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs index ee52d37851..76fe0977d7 100644 --- a/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs +++ b/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.CacheManager { - public class OutputCacheMiddlewareRealCacheTests + public class OutputCacheMiddlewareRealCacheTests : UnitTest { private readonly IOcelotCache _cacheManager; private readonly ICacheKeyGenerator _cacheKeyGenerator; @@ -77,7 +77,7 @@ private void GivenTheDownstreamRouteIs() { var route = new DownstreamRouteBuilder() .WithIsCached(true) - .WithCacheOptions(new CacheOptions(100, "kanken", null)) + .WithCacheOptions(new CacheOptions(100, "kanken", null, false)) .WithUpstreamHttpMethod(new List { "Get" }) .Build(); diff --git a/test/Ocelot.UnitTests/Claims/AddClaimsToRequestTests.cs b/test/Ocelot.UnitTests/Claims/AddClaimsToRequestTests.cs index c3d2c212e9..362df7f4a3 100644 --- a/test/Ocelot.UnitTests/Claims/AddClaimsToRequestTests.cs +++ b/test/Ocelot.UnitTests/Claims/AddClaimsToRequestTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.Claims { - public class AddClaimsToRequestTests + public class AddClaimsToRequestTests : UnitTest { private readonly AddClaimsToRequest _addClaimsToRequest; private readonly Mock _parser; diff --git a/test/Ocelot.UnitTests/Claims/ClaimsToClaimsMiddlewareTests.cs b/test/Ocelot.UnitTests/Claims/ClaimsToClaimsMiddlewareTests.cs index e996968af4..2fcface833 100644 --- a/test/Ocelot.UnitTests/Claims/ClaimsToClaimsMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Claims/ClaimsToClaimsMiddlewareTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Claims { - public class ClaimsToClaimsMiddlewareTests + public class ClaimsToClaimsMiddlewareTests : UnitTest { private readonly Mock _addHeaders; private readonly Mock _loggerFactory; diff --git a/test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs index d4605f0380..469e3d7f27 100644 --- a/test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.Configuration { - public class AggregatesCreatorTests + public class AggregatesCreatorTests : UnitTest { private readonly AggregatesCreator _creator; private readonly Mock _utpCreator; diff --git a/test/Ocelot.UnitTests/Configuration/AuthenticationOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/AuthenticationOptionsCreatorTests.cs index b1830ba620..d0e8d599e8 100644 --- a/test/Ocelot.UnitTests/Configuration/AuthenticationOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/AuthenticationOptionsCreatorTests.cs @@ -46,7 +46,7 @@ public void Create_OptionsObjIsNotNull_CreatedSuccessfully(bool isAuthentication // Arrange string authenticationProviderKey = !isAuthenticationProviderKeys ? "Test" : null; string[] authenticationProviderKeys = isAuthenticationProviderKeys ? - ["Test #1", "Test #2"] : null; + new string[] { "Test #1", "Test #2" } : null; var fileRoute = new FileRoute() { AuthenticationOptions = new FileAuthenticationOptions diff --git a/test/Ocelot.UnitTests/Configuration/CacheOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/CacheOptionsCreatorTests.cs new file mode 100644 index 0000000000..12ed54cdf3 --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/CacheOptionsCreatorTests.cs @@ -0,0 +1,100 @@ +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; + +namespace Ocelot.UnitTests.Configuration; + +[Trait("Feat", "2058")] +[Trait("Bug", "2059")] +public class CacheOptionsCreatorTests +{ + [Fact] + public void ShouldCreateCacheOptions() + { + var options = FileCacheOptionsFactory(); + var cacheOptionsCreator = new CacheOptionsCreator(); + var result = cacheOptionsCreator.Create(options, null, null, null); + + result.TtlSeconds.ShouldBe(options.TtlSeconds.Value); + result.Region.ShouldBe(options.Region); + result.Header.ShouldBe(options.Header); + result.EnableContentHashing.ShouldBe(options.EnableContentHashing.Value); + } + + [Fact] + public void ShouldCreateCacheOptionsUsingGlobalConfiguration() + { + var global = GlobalConfigurationFactory(); + + var cacheOptionsCreator = new CacheOptionsCreator(); + var result = cacheOptionsCreator.Create(new FileCacheOptions(), global, null, null); + + result.TtlSeconds.ShouldBe(global.CacheOptions.TtlSeconds.Value); + result.Region.ShouldBe(global.CacheOptions.Region); + result.Header.ShouldBe(global.CacheOptions.Header); + result.EnableContentHashing.ShouldBe(global.CacheOptions.EnableContentHashing.Value); + } + + [Fact] + public void RouteCacheOptionsShouldOverrideGlobalConfiguration() + { + var global = GlobalConfigurationFactory(); + var options = FileCacheOptionsFactory(); + + var cacheOptionsCreator = new CacheOptionsCreator(); + var result = cacheOptionsCreator.Create(options, global, null, null); + + result.TtlSeconds.ShouldBe(options.TtlSeconds.Value); + result.Region.ShouldBe(options.Region); + result.Header.ShouldBe(options.Header); + result.EnableContentHashing.ShouldBe(options.EnableContentHashing.Value); + } + + [Fact] + public void ShouldCreateCacheOptionsWithDefaults() + { + var cacheOptionsCreator = new CacheOptionsCreator(); + var result = cacheOptionsCreator.Create(new FileCacheOptions(), null, "/", new List { "GET" }); + + result.TtlSeconds.ShouldBe(0); + result.Region.ShouldBe("GET"); + result.Header.ShouldBe(null); + result.EnableContentHashing.ShouldBe(false); + } + + [Fact] + public void ShouldComputeRegionIfNotProvided() + { + var global = GlobalConfigurationFactory(); + var options = FileCacheOptionsFactory(); + + global.CacheOptions.Region = null; + options.Region = null; + + var cacheOptionsCreator = new CacheOptionsCreator(); + var result = cacheOptionsCreator.Create(options, global, "/api/values", new List { "GET", "POST" }); + + result.TtlSeconds.ShouldBe(options.TtlSeconds.Value); + result.Region.ShouldBe("GETPOSTapivalues"); + result.Header.ShouldBe(options.Header); + result.EnableContentHashing.ShouldBe(options.EnableContentHashing.Value); + } + + private static FileGlobalConfiguration GlobalConfigurationFactory() => new() + { + CacheOptions = new FileCacheOptions + { + TtlSeconds = 20, + Region = "globalRegion", + Header = "globalHeader", + EnableContentHashing = false, + }, + }; + + private static FileCacheOptions FileCacheOptionsFactory() => new() + { + TtlSeconds = 10, + Region = "region", + Header = "header", + EnableContentHashing = true, + }; +} diff --git a/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenSourceTests.cs b/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenSourceTests.cs index 299eb30ffc..dd0b22bd3e 100644 --- a/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenSourceTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenSourceTests.cs @@ -2,7 +2,7 @@ namespace Ocelot.UnitTests.Configuration.ChangeTracking { - public class OcelotConfigurationChangeTokenSourceTests + public class OcelotConfigurationChangeTokenSourceTests : UnitTest { private readonly IOcelotConfigurationChangeTokenSource _source; diff --git a/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenTests.cs b/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenTests.cs index 58b20d7ea8..20602553e9 100644 --- a/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenTests.cs @@ -2,7 +2,7 @@ namespace Ocelot.UnitTests.Configuration.ChangeTracking { - public class OcelotConfigurationChangeTokenTests + public class OcelotConfigurationChangeTokenTests : UnitTest { [Fact] public void should_call_callback_with_state() diff --git a/test/Ocelot.UnitTests/Configuration/ClaimToThingConfigurationParserTests.cs b/test/Ocelot.UnitTests/Configuration/ClaimToThingConfigurationParserTests.cs index 6b46b0fff7..553a32e897 100644 --- a/test/Ocelot.UnitTests/Configuration/ClaimToThingConfigurationParserTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ClaimToThingConfigurationParserTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.Configuration { - public class ClaimToThingConfigurationParserTests + public class ClaimToThingConfigurationParserTests : UnitTest { private Dictionary _dictionary; private readonly IClaimToThingConfigurationParser _claimToThingConfigurationParser; diff --git a/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs index 70438aed84..878d1da77a 100644 --- a/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.Configuration { - public class ClaimsToThingCreatorTests + public class ClaimsToThingCreatorTests : UnitTest { private readonly Mock _configParser; private Dictionary _claimsToThings; diff --git a/test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs index 2af3224df8..8f6c7f803e 100644 --- a/test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.Configuration { - public class ConfigurationCreatorTests + public class ConfigurationCreatorTests : UnitTest { private ConfigurationCreator _creator; private InternalConfiguration _result; diff --git a/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs index 1be67622c6..197ab70b2e 100644 --- a/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs @@ -3,128 +3,129 @@ using Ocelot.Configuration.ChangeTracking; using Ocelot.Configuration.File; using Ocelot.Configuration.Repository; +using Ocelot.DependencyInjection; +using System.Runtime.CompilerServices; namespace Ocelot.UnitTests.Configuration { - public class DiskFileConfigurationRepositoryTests : IDisposable + public sealed class DiskFileConfigurationRepositoryTests : FileUnitTest { private readonly Mock _hostingEnvironment; private readonly Mock _changeTokenSource; private IFileConfigurationRepository _repo; - private string _environmentSpecificPath; - private string _ocelotJsonPath; private FileConfiguration _result; - private FileConfiguration _fileConfiguration; - - // This is a bit dirty and it is dev.dev so that the ConfigurationBuilderExtensionsTests - // cant pick it up if they run in parralel..and the semaphore stops them running at the same time...sigh - // these are not really unit tests but whatever... - private string _environmentName = "DEV.DEV"; - - private static SemaphoreSlim _semaphore; public DiskFileConfigurationRepositoryTests() { - _semaphore = new SemaphoreSlim(1, 1); - _semaphore.Wait(); _hostingEnvironment = new Mock(); - _hostingEnvironment.Setup(he => he.EnvironmentName).Returns(_environmentName); _changeTokenSource = new Mock(MockBehavior.Strict); _changeTokenSource.Setup(m => m.Activate()); - _repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object, _changeTokenSource.Object); + } + + private void Arrange([CallerMemberName] string testName = null) + { + _hostingEnvironment.Setup(he => he.EnvironmentName).Returns(testName); + _repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object, _changeTokenSource.Object, TestID); } [Fact] - public void should_return_file_configuration() + public void Should_return_file_configuration() { + Arrange(); var config = FakeFileConfigurationForGet(); + GivenTheConfigurationIs(config); - this.Given(_ => GivenTheConfigurationIs(config)) - .When(_ => WhenIGetTheRoutes()) - .Then(_ => ThenTheFollowingIsReturned(config)) - .BDDfy(); + // Act + WhenIGetTheRoutes(); + + // Assert + ThenTheFollowingIsReturned(config); } [Fact] - public void should_return_file_configuration_if_environment_name_is_unavailable() + public void Should_return_file_configuration_if_environment_name_is_unavailable() { + Arrange(); var config = FakeFileConfigurationForGet(); + GivenTheEnvironmentNameIsUnavailable(); + GivenTheConfigurationIs(config); - this.Given(_ => GivenTheEnvironmentNameIsUnavailable()) - .And(_ => GivenTheConfigurationIs(config)) - .When(_ => WhenIGetTheRoutes()) - .Then(_ => ThenTheFollowingIsReturned(config)) - .BDDfy(); + // Act + WhenIGetTheRoutes(); + + // Assert + ThenTheFollowingIsReturned(config); } [Fact] - public void should_set_file_configuration() + public void Should_set_file_configuration() { + Arrange(); var config = FakeFileConfigurationForSet(); - this.Given(_ => GivenIHaveAConfiguration(config)) - .When(_ => WhenISetTheConfiguration()) - .Then(_ => ThenTheConfigurationIsStoredAs(config)) - .And(_ => ThenTheConfigurationJsonIsIndented(config)) - .And(x => AndTheChangeTokenIsActivated()) - .BDDfy(); + // Act + WhenISetTheConfiguration(config); + + // Assert + ThenTheConfigurationIsStoredAs(config); + ThenTheConfigurationJsonIsIndented(config); + AndTheChangeTokenIsActivated(); } [Fact] - public void should_set_file_configuration_if_environment_name_is_unavailable() + public void Should_set_file_configuration_if_environment_name_is_unavailable() { + Arrange(); var config = FakeFileConfigurationForSet(); + GivenTheEnvironmentNameIsUnavailable(); - this.Given(_ => GivenIHaveAConfiguration(config)) - .And(_ => GivenTheEnvironmentNameIsUnavailable()) - .When(_ => WhenISetTheConfiguration()) - .Then(_ => ThenTheConfigurationIsStoredAs(config)) - .And(_ => ThenTheConfigurationJsonIsIndented(config)) - .BDDfy(); + // Act + WhenISetTheConfiguration(config); + + // Assert + ThenTheConfigurationIsStoredAs(config); + ThenTheConfigurationJsonIsIndented(config); } [Fact] - public void should_set_environment_file_configuration_and_ocelot_file_configuration() + public void Should_set_environment_file_configuration_and_ocelot_file_configuration() { + Arrange(); var config = FakeFileConfigurationForSet(); + GivenTheConfigurationIs(config); + var ocelotJson = GivenTheUserAddedOcelotJson(); - this.Given(_ => GivenIHaveAConfiguration(config)) - .And(_ => GivenTheConfigurationIs(config)) - .And(_ => GivenTheUserAddedOcelotJson()) - .When(_ => WhenISetTheConfiguration()) - .Then(_ => ThenTheConfigurationIsStoredAs(config)) - .And(_ => ThenTheConfigurationJsonIsIndented(config)) - .Then(_ => ThenTheOcelotJsonIsStoredAs(config)) - .BDDfy(); + // Act + WhenISetTheConfiguration(config); + + // Assert + ThenTheConfigurationIsStoredAs(config); + ThenTheConfigurationJsonIsIndented(config); + ThenTheOcelotJsonIsStoredAs(ocelotJson, config); } - private void GivenTheUserAddedOcelotJson() + private FileInfo GivenTheUserAddedOcelotJson() { - _ocelotJsonPath = $"{AppContext.BaseDirectory}/ocelot.json"; - - if (File.Exists(_ocelotJsonPath)) + var primaryFile = Path.Combine(TestID, ConfigurationBuilderExtensions.PrimaryConfigFile); + var ocelotJson = new FileInfo(primaryFile); + if (ocelotJson.Exists) { - File.Delete(_ocelotJsonPath); + ocelotJson.Delete(); } - File.WriteAllText(_ocelotJsonPath, "Doesnt matter"); + File.WriteAllText(ocelotJson.FullName, "Doesnt matter"); + _files.Add(ocelotJson.FullName); + return ocelotJson; } private void GivenTheEnvironmentNameIsUnavailable() { - _environmentName = null; - _hostingEnvironment.Setup(he => he.EnvironmentName).Returns(_environmentName); - _repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object, _changeTokenSource.Object); - } - - private void GivenIHaveAConfiguration(FileConfiguration fileConfiguration) - { - _fileConfiguration = fileConfiguration; + _hostingEnvironment.Setup(he => he.EnvironmentName).Returns((string)null); } - private void WhenISetTheConfiguration() + private void WhenISetTheConfiguration(FileConfiguration fileConfiguration) { - _repo.SetAsync(_fileConfiguration); + _repo.SetAsync(fileConfiguration); _result = _repo.GetAsync().Result; } @@ -151,34 +152,34 @@ private void ThenTheConfigurationIsStoredAs(FileConfiguration expecteds) } } - private void ThenTheOcelotJsonIsStoredAs(FileConfiguration expecteds) + private void ThenTheOcelotJsonIsStoredAs(FileInfo ocelotJson, FileConfiguration expecteds) { - var resultText = File.ReadAllText(_ocelotJsonPath); + var actual = File.ReadAllText(ocelotJson.FullName); var expectedText = JsonConvert.SerializeObject(expecteds, Formatting.Indented); - resultText.ShouldBe(expectedText); + actual.ShouldBe(expectedText); } - private void GivenTheConfigurationIs(FileConfiguration fileConfiguration) + private void GivenTheConfigurationIs(FileConfiguration fileConfiguration, [CallerMemberName] string environmentName = null) { - _environmentSpecificPath = $"{AppContext.BaseDirectory}/ocelot{(string.IsNullOrEmpty(_environmentName) ? string.Empty : ".")}{_environmentName}.json"; - + var environmentSpecificPath = Path.Combine(TestID, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, environmentName)); var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); - - if (File.Exists(_environmentSpecificPath)) + var environmentSpecific = new FileInfo(environmentSpecificPath); + if (environmentSpecific.Exists) { - File.Delete(_environmentSpecificPath); + environmentSpecific.Delete(); } - File.WriteAllText(_environmentSpecificPath, jsonConfiguration); + File.WriteAllText(environmentSpecific.FullName, jsonConfiguration); + _files.Add(environmentSpecific.FullName); } - private void ThenTheConfigurationJsonIsIndented(FileConfiguration expecteds) + private void ThenTheConfigurationJsonIsIndented(FileConfiguration expecteds, [CallerMemberName] string environmentName = null) { - var path = !string.IsNullOrEmpty(_environmentSpecificPath) ? _environmentSpecificPath : _environmentSpecificPath = $"{AppContext.BaseDirectory}/ocelot{(string.IsNullOrEmpty(_environmentName) ? string.Empty : ".")}{_environmentName}.json"; - - var resultText = File.ReadAllText(path); + var environmentSpecific = Path.Combine(TestID, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, environmentName)); + var actual = File.ReadAllText(environmentSpecific); var expectedText = JsonConvert.SerializeObject(expecteds, Formatting.Indented); - resultText.ShouldBe(expectedText); + actual.ShouldBe(expectedText); + _files.Add(environmentSpecific); } private void WhenIGetTheRoutes() @@ -216,79 +217,34 @@ private void AndTheChangeTokenIsActivated() private static FileConfiguration FakeFileConfigurationForSet() { - var routes = new List - { - new() - { - DownstreamHostAndPorts = new List - { - new() - { - Host = "123.12.12.12", - Port = 80, - }, - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/asdfs/test/{test}", - }, - }; - - var globalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "https", - Port = 198, - Host = "blah", - }, - }; - - return new FileConfiguration - { - GlobalConfiguration = globalConfiguration, - Routes = routes, - }; + var route = GivenRoute("123.12.12.12", "/asdfs/test/{test}"); + return GivenConfiguration(route); } private static FileConfiguration FakeFileConfigurationForGet() { - var routes = new List - { - new() - { - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = 80, - }, - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/test/test/{test}", - }, - }; + var route = GivenRoute("localhost", "/test/test/{test}"); + return GivenConfiguration(route); + } - var globalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "https", - Port = 198, - Host = "blah", - }, - }; + private static FileRoute GivenRoute(string host, string downstream) => new() + { + DownstreamHostAndPorts = new() { new(host, 80) }, + DownstreamScheme = Uri.UriSchemeHttps, + DownstreamPathTemplate = downstream, + }; - return new FileConfiguration + private static FileConfiguration GivenConfiguration(params FileRoute[] routes) + { + var config = new FileConfiguration(); + config.Routes.AddRange(routes); + config.GlobalConfiguration.ServiceDiscoveryProvider = new() { - GlobalConfiguration = globalConfiguration, - Routes = routes, + Scheme = "https", + Port = 198, + Host = "blah", }; - } - - public void Dispose() - { - _semaphore.Release(); + return config; } } } diff --git a/test/Ocelot.UnitTests/Configuration/DownstreamAddressesCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/DownstreamAddressesCreatorTests.cs index 6cb6ece1ca..0fe22959f3 100644 --- a/test/Ocelot.UnitTests/Configuration/DownstreamAddressesCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DownstreamAddressesCreatorTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Configuration { - public class DownstreamAddressesCreatorTests + public class DownstreamAddressesCreatorTests : UnitTest { public DownstreamAddressesCreator _creator; private FileRoute _route; diff --git a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs index 1a8b315667..aec6ffb751 100644 --- a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.Configuration { - public class DynamicsCreatorTests + public class DynamicsCreatorTests : UnitTest { private readonly DynamicsCreator _creator; private readonly Mock _rloCreator; diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationPollerTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationPollerTests.cs index 0f53840f9d..b4172a81b6 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationPollerTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationPollerTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Configuration { - public class FileConfigurationPollerTests : IDisposable + public class FileConfigurationPollerTests : UnitTest, IDisposable { private readonly FileConfigurationPoller _poller; private readonly Mock _factory; diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs index 016b9868bb..44051894c7 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Configuration { - public class FileConfigurationSetterTests + public class FileConfigurationSetterTests : UnitTest { private FileConfiguration _fileConfiguration; private readonly FileAndInternalConfigurationSetter _configSetter; diff --git a/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs index 8656040711..b8fb5b7f6c 100644 --- a/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Configuration { - public class FileInternalConfigurationCreatorTests + public class FileInternalConfigurationCreatorTests : UnitTest { private readonly Mock _validator; private readonly Mock _routesCreator; diff --git a/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs index 854f2abea9..b5f42d6e3e 100644 --- a/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.Configuration { - public class HeaderFindAndReplaceCreatorTests + public class HeaderFindAndReplaceCreatorTests : UnitTest { private readonly HeaderFindAndReplaceCreator _creator; private FileRoute _route; diff --git a/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs index 8e41eec9ef..33f6d629a8 100644 --- a/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.Configuration { - public class HttpHandlerOptionsCreatorTests + public class HttpHandlerOptionsCreatorTests : UnitTest { private IHttpHandlerOptionsCreator _httpHandlerOptionsCreator; private FileRoute _fileRoute; diff --git a/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs index a9af24523f..85d6bf210b 100644 --- a/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.Configuration { - public class InMemoryConfigurationRepositoryTests + public class InMemoryConfigurationRepositoryTests : UnitTest { private readonly InMemoryInternalConfigurationRepository _repo; private IInternalConfiguration _config; diff --git a/test/Ocelot.UnitTests/Configuration/LoadBalancerOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/LoadBalancerOptionsCreatorTests.cs index d7d2e1bac6..7e1464ecd8 100644 --- a/test/Ocelot.UnitTests/Configuration/LoadBalancerOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/LoadBalancerOptionsCreatorTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Configuration { - public class LoadBalancerOptionsCreatorTests + public class LoadBalancerOptionsCreatorTests : UnitTest { private readonly ILoadBalancerOptionsCreator _creator; private FileLoadBalancerOptions _fileLoadBalancerOptions; diff --git a/test/Ocelot.UnitTests/Configuration/QoSOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/QoSOptionsCreatorTests.cs index 893c8630b7..f394932dfd 100644 --- a/test/Ocelot.UnitTests/Configuration/QoSOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/QoSOptionsCreatorTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.Configuration { - public class QoSOptionsCreatorTests + public class QoSOptionsCreatorTests : UnitTest { private readonly QoSOptionsCreator _creator; private FileRoute _fileRoute; diff --git a/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs index 63e779312c..e514877d2b 100644 --- a/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.Configuration { - public class RateLimitOptionsCreatorTests + public class RateLimitOptionsCreatorTests : UnitTest { private FileRoute _fileRoute; private FileGlobalConfiguration _fileGlobalConfig; diff --git a/test/Ocelot.UnitTests/Configuration/RequestIdKeyCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RequestIdKeyCreatorTests.cs index 4511c04fae..d2be4fcc5a 100644 --- a/test/Ocelot.UnitTests/Configuration/RequestIdKeyCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RequestIdKeyCreatorTests.cs @@ -3,7 +3,7 @@ namespace Ocelot.UnitTests.Configuration { - public class RequestIdKeyCreatorTests + public class RequestIdKeyCreatorTests : UnitTest { private FileRoute _fileRoute; private FileGlobalConfiguration _fileGlobalConfig; diff --git a/test/Ocelot.UnitTests/Configuration/RouteKeyCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RouteKeyCreatorTests.cs index 54f63dd4ce..ff26b406f0 100644 --- a/test/Ocelot.UnitTests/Configuration/RouteKeyCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RouteKeyCreatorTests.cs @@ -1,10 +1,10 @@ -using Ocelot.Configuration.Creator; -using Ocelot.Configuration.File; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.LoadBalancer.LoadBalancers; namespace Ocelot.UnitTests.Configuration { - public class RouteKeyCreatorTests + public class RouteKeyCreatorTests : UnitTest { private readonly RouteKeyCreator _creator; private FileRoute _route; @@ -16,7 +16,7 @@ public RouteKeyCreatorTests() } [Fact] - public void should_return_sticky_session_key() + public void Should_return_sticky_session_key() { var route = new FileRoute { @@ -29,35 +29,85 @@ public void should_return_sticky_session_key() this.Given(_ => GivenThe(route)) .When(_ => WhenICreate()) - .Then(_ => ThenTheResultIs($"{nameof(CookieStickySessions)}:{route.LoadBalancerOptions.Key}")) + .Then(_ => ThenTheResultIs("CookieStickySessions:testy")) .BDDfy(); } [Fact] - public void should_return_re_route_key() + public void Should_return_route_key() { var route = new FileRoute { UpstreamPathTemplate = "/api/product", - UpstreamHttpMethod = new List { "GET", "POST", "PUT" }, - DownstreamHostAndPorts = new List + UpstreamHttpMethod = new() { "GET", "POST", "PUT" }, + DownstreamHostAndPorts = new() { - new() - { - Host = "localhost", - Port = 123, - }, - new() - { - Host = "localhost", - Port = 123, - }, + new("localhost", 8080), + new("localhost", 4430), }, }; this.Given(_ => GivenThe(route)) .When(_ => WhenICreate()) - .Then(_ => ThenTheResultIs($"{route.UpstreamPathTemplate}|{string.Join(',', route.UpstreamHttpMethod)}|{string.Join(',', route.DownstreamHostAndPorts.Select(x => $"{x.Host}:{x.Port}"))}")) + .Then(_ => ThenTheResultIs("GET,POST,PUT|/api/product|no-host|localhost:8080,localhost:4430|no-svc-ns|no-svc-name|no-lb-type|no-lb-key")) + .BDDfy(); + } + + [Fact] + public void Should_return_route_key_with_upstream_host() + { + var route = new FileRoute + { + UpstreamHost = "my-host", + UpstreamPathTemplate = "/api/product", + UpstreamHttpMethod = new() { "GET", "POST", "PUT" }, + DownstreamHostAndPorts = new() + { + new("localhost", 8080), + new("localhost", 4430), + }, + }; + + this.Given(_ => GivenThe(route)) + .When(_ => WhenICreate()) + .Then(_ => ThenTheResultIs("GET,POST,PUT|/api/product|my-host|localhost:8080,localhost:4430|no-svc-ns|no-svc-name|no-lb-type|no-lb-key")) + .BDDfy(); + } + + [Fact] + public void Should_return_route_key_with_svc_name() + { + var route = new FileRoute + { + UpstreamPathTemplate = "/api/product", + UpstreamHttpMethod = new() { "GET", "POST", "PUT" }, + ServiceName = "products-service", + }; + + this.Given(_ => GivenThe(route)) + .When(_ => WhenICreate()) + .Then(_ => ThenTheResultIs("GET,POST,PUT|/api/product|no-host|no-host-and-port|no-svc-ns|products-service|no-lb-type|no-lb-key")) + .BDDfy(); + } + + [Fact] + public void Should_return_route_key_with_load_balancer_options() + { + var route = new FileRoute + { + UpstreamPathTemplate = "/api/product", + UpstreamHttpMethod = new() { "GET", "POST", "PUT" }, + ServiceName = "products-service", + LoadBalancerOptions = new FileLoadBalancerOptions + { + Type = nameof(LeastConnection), + Key = "testy", + }, + }; + + this.Given(_ => GivenThe(route)) + .When(_ => WhenICreate()) + .Then(_ => ThenTheResultIs("GET,POST,PUT|/api/product|no-host|no-host-and-port|no-svc-ns|products-service|LeastConnection|testy")) .BDDfy(); } diff --git a/test/Ocelot.UnitTests/Configuration/RouteOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RouteOptionsCreatorTests.cs index bfb2927e5e..21f9058ae0 100644 --- a/test/Ocelot.UnitTests/Configuration/RouteOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RouteOptionsCreatorTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Configuration; -public class RouteOptionsCreatorTests +public class RouteOptionsCreatorTests : UnitTest { private readonly RouteOptionsCreator _creator; @@ -126,7 +126,7 @@ public void Create_RouteOptions_HappyPath(bool isAuthenticationProviderKeys) { AuthenticationProviderKey = !isAuthenticationProviderKeys ? "Test" : null, AuthenticationProviderKeys = isAuthenticationProviderKeys ? - [string.Empty, "Test #1"] : null, + new string[] { string.Empty, "Test #1" } : null, }, RouteClaimsRequirement = new Dictionary { diff --git a/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs index cf5f38e7ba..11b5fc3e03 100644 --- a/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs @@ -1,5 +1,4 @@ -using Ocelot.Cache; -using Ocelot.Configuration; +using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; @@ -7,7 +6,7 @@ namespace Ocelot.UnitTests.Configuration { - public class RoutesCreatorTests + public class RoutesCreatorTests : UnitTest { private readonly RoutesCreator _creator; private readonly Mock _cthCreator; @@ -17,7 +16,7 @@ public class RoutesCreatorTests private readonly Mock _qosoCreator; private readonly Mock _rroCreator; private readonly Mock _rloCreator; - private readonly Mock _rCreator; + private readonly Mock _coCreator; private readonly Mock _hhoCreator; private readonly Mock _hfarCreator; private readonly Mock _daCreator; @@ -34,7 +33,7 @@ public class RoutesCreatorTests private List _ctt; private QoSOptions _qoso; private RateLimitOptions _rlo; - private string _region; + private CacheOptions _cacheOptions; private HttpHandlerOptions _hho; private HeaderTransformations _ht; private List _dhp; @@ -51,7 +50,7 @@ public RoutesCreatorTests() _qosoCreator = new Mock(); _rroCreator = new Mock(); _rloCreator = new Mock(); - _rCreator = new Mock(); + _coCreator = new Mock(); _hhoCreator = new Mock(); _hfarCreator = new Mock(); _daCreator = new Mock(); @@ -68,7 +67,7 @@ public RoutesCreatorTests() _qosoCreator.Object, _rroCreator.Object, _rloCreator.Object, - _rCreator.Object, + _coCreator.Object, _hhoCreator.Object, _hfarCreator.Object, _daCreator.Object, @@ -161,7 +160,8 @@ private void GivenTheDependenciesAreSetUpCorrectly() _ctt = new List(); _qoso = new QoSOptionsBuilder().Build(); _rlo = new RateLimitOptionsBuilder().Build(); - _region = "vesty"; + + _cacheOptions = new CacheOptions(0, "vesty", null, false); _hho = new HttpHandlerOptionsBuilder().Build(); _ht = new HeaderTransformations(new List(), new List(), new List(), new List()); _dhp = new List(); @@ -175,7 +175,7 @@ private void GivenTheDependenciesAreSetUpCorrectly() _cthCreator.Setup(x => x.Create(It.IsAny>())).Returns(_ctt); _qosoCreator.Setup(x => x.Create(It.IsAny(), It.IsAny(), It.IsAny>())).Returns(_qoso); _rloCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_rlo); - _rCreator.Setup(x => x.Create(It.IsAny())).Returns(_region); + _coCreator.Setup(x => x.Create(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())).Returns(_cacheOptions); _hhoCreator.Setup(x => x.Create(It.IsAny())).Returns(_hho); _hfarCreator.Setup(x => x.Create(It.IsAny())).Returns(_ht); _daCreator.Setup(x => x.Create(It.IsAny())).Returns(_dhp); @@ -222,8 +222,8 @@ private void ThenTheRouteIsSet(FileRoute expected, int routeIndex) _result[routeIndex].DownstreamRoute[0].ClaimsToClaims.ShouldBe(_ctt); _result[routeIndex].DownstreamRoute[0].QosOptions.ShouldBe(_qoso); _result[routeIndex].DownstreamRoute[0].RateLimitOptions.ShouldBe(_rlo); - _result[routeIndex].DownstreamRoute[0].CacheOptions.Region.ShouldBe(_region); - _result[routeIndex].DownstreamRoute[0].CacheOptions.TtlSeconds.ShouldBe(expected.FileCacheOptions.TtlSeconds); + _result[routeIndex].DownstreamRoute[0].CacheOptions.Region.ShouldBe(_cacheOptions.Region); + _result[routeIndex].DownstreamRoute[0].CacheOptions.TtlSeconds.ShouldBe(0); _result[routeIndex].DownstreamRoute[0].HttpHandlerOptions.ShouldBe(_hho); _result[routeIndex].DownstreamRoute[0].UpstreamHeadersFindAndReplace.ShouldBe(_ht.Upstream); _result[routeIndex].DownstreamRoute[0].DownstreamHeadersFindAndReplace.ShouldBe(_ht.Downstream); @@ -264,7 +264,7 @@ private void ThenTheDepsAreCalledFor(FileRoute fileRoute, FileGlobalConfiguratio _cthCreator.Verify(x => x.Create(fileRoute.AddQueriesToRequest), Times.Once); _qosoCreator.Verify(x => x.Create(fileRoute.QoSOptions, fileRoute.UpstreamPathTemplate, fileRoute.UpstreamHttpMethod)); _rloCreator.Verify(x => x.Create(fileRoute.RateLimitOptions, globalConfig), Times.Once); - _rCreator.Verify(x => x.Create(fileRoute), Times.Once); + _coCreator.Verify(x => x.Create(fileRoute.FileCacheOptions, globalConfig, fileRoute.UpstreamPathTemplate, fileRoute.UpstreamHttpMethod), Times.Once); _hhoCreator.Verify(x => x.Create(fileRoute.HttpHandlerOptions), Times.Once); _hfarCreator.Verify(x => x.Create(fileRoute), Times.Once); _daCreator.Verify(x => x.Create(fileRoute), Times.Once); diff --git a/test/Ocelot.UnitTests/Configuration/SecurityOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/SecurityOptionsCreatorTests.cs index 6c29680201..81d9fd36da 100644 --- a/test/Ocelot.UnitTests/Configuration/SecurityOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/SecurityOptionsCreatorTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Configuration { - public class SecurityOptionsCreatorTests + public class SecurityOptionsCreatorTests : UnitTest { private FileRoute _fileRoute; private SecurityOptions _result; diff --git a/test/Ocelot.UnitTests/Configuration/ServiceProviderCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/ServiceProviderCreatorTests.cs index ac92db2063..aaa238cca9 100644 --- a/test/Ocelot.UnitTests/Configuration/ServiceProviderCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ServiceProviderCreatorTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.Configuration { - public class ServiceProviderCreatorTests + public class ServiceProviderCreatorTests : UnitTest { private readonly ServiceProviderConfigurationCreator _creator; private FileGlobalConfiguration _globalConfig; diff --git a/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs index 4028d456bf..76cb789d32 100644 --- a/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Configuration { - public class UpstreamTemplatePatternCreatorTests + public class UpstreamTemplatePatternCreatorTests : UnitTest { private FileRoute _fileRoute; private readonly UpstreamTemplatePatternCreator _creator; diff --git a/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs index 77d2bf7913..3a87dec2f6 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs @@ -18,17 +18,17 @@ namespace Ocelot.UnitTests.Configuration.Validation { - public class FileConfigurationFluentValidatorTests + public class FileConfigurationFluentValidatorTests : UnitTest { private IConfigurationValidator _configurationValidator; private FileConfiguration _fileConfiguration; private Response _result; private IServiceProvider _provider; private readonly ServiceCollection _services; - private readonly Mock _authProvider; + private readonly Mock _authProvider; public FileConfigurationFluentValidatorTests() - { + { _services = new ServiceCollection(); _authProvider = new Mock(); _provider = _services.BuildServiceProvider(); @@ -38,8 +38,8 @@ public FileConfigurationFluentValidatorTests() } [Fact] - public void configuration_is_valid_if_service_discovery_options_specified_and_has_service_fabric_as_option() - { + public void Configuration_is_valid_if_service_discovery_options_specified_and_has_service_fabric_as_option() + { var route = GivenServiceDiscoveryRoute(); var configuration = GivenAConfiguration(route); configuration.GlobalConfiguration.ServiceDiscoveryProvider = GivenDefaultServiceDiscoveryProvider(); @@ -50,7 +50,7 @@ public void configuration_is_valid_if_service_discovery_options_specified_and_ha } [Fact] - public void configuration_is_valid_if_service_discovery_options_specified_and_has_service_discovery_handler() + public void Configuration_is_valid_if_service_discovery_options_specified_and_has_service_discovery_handler() { var route = GivenServiceDiscoveryRoute(); var configuration = GivenAConfiguration(route); @@ -64,7 +64,7 @@ public void configuration_is_valid_if_service_discovery_options_specified_and_ha } [Fact] - public void configuration_is_valid_if_service_discovery_options_specified_dynamically_and_has_service_discovery_handler() + public void Configuration_is_valid_if_service_discovery_options_specified_dynamically_and_has_service_discovery_handler() { var configuration = new FileConfiguration(); configuration.GlobalConfiguration.ServiceDiscoveryProvider = GivenDefaultServiceDiscoveryProvider(); @@ -77,7 +77,7 @@ public void configuration_is_valid_if_service_discovery_options_specified_dynami } [Fact] - public void configuration_is_invalid_if_service_discovery_options_specified_but_no_service_discovery_handler() + public void Configuration_is_invalid_if_service_discovery_options_specified_but_no_service_discovery_handler() { var route = GivenServiceDiscoveryRoute(); var configuration = GivenAConfiguration(route); @@ -92,7 +92,7 @@ public void configuration_is_invalid_if_service_discovery_options_specified_but_ } [Fact] - public void configuration_is_invalid_if_service_discovery_options_specified_dynamically_but_service_discovery_handler() + public void Configuration_is_invalid_if_service_discovery_options_specified_dynamically_but_service_discovery_handler() { var configuration = new FileConfiguration(); configuration.GlobalConfiguration.ServiceDiscoveryProvider = GivenDefaultServiceDiscoveryProvider(); @@ -106,8 +106,8 @@ public void configuration_is_invalid_if_service_discovery_options_specified_dyna } [Fact] - public void configuration_is_invalid_if_service_discovery_options_specified_but_no_service_discovery_handler_with_matching_name() - { + public void Configuration_is_invalid_if_service_discovery_options_specified_but_no_service_discovery_handler_with_matching_name() + { var route = GivenServiceDiscoveryRoute(); var configuration = GivenAConfiguration(route); configuration.GlobalConfiguration.ServiceDiscoveryProvider = GivenDefaultServiceDiscoveryProvider(); @@ -122,12 +122,12 @@ public void configuration_is_invalid_if_service_discovery_options_specified_but_ } [Fact] - public void configuration_is_valid_if_qos_options_specified_and_has_qos_handler() - { + public void Configuration_is_valid_if_qos_options_specified_and_has_qos_handler() + { var route = GivenDefaultRoute("/laura", "/"); route.Key = "Laura"; route.QoSOptions = new FileQoSOptions - { + { TimeoutValue = 1, ExceptionsAllowedBeforeBreaking = 1, }; @@ -139,13 +139,13 @@ public void configuration_is_valid_if_qos_options_specified_and_has_qos_handler( } [Fact] - public void configuration_is_valid_if_qos_options_specified_globally_and_has_qos_handler() + public void Configuration_is_valid_if_qos_options_specified_globally_and_has_qos_handler() { var route = GivenDefaultRoute("/laura", "/"); route.Key = "Laura"; var configuration = GivenAConfiguration(route); configuration.GlobalConfiguration.QoSOptions = new FileQoSOptions - { + { TimeoutValue = 1, ExceptionsAllowedBeforeBreaking = 1, }; @@ -157,12 +157,12 @@ public void configuration_is_valid_if_qos_options_specified_globally_and_has_qos } [Fact] - public void configuration_is_invalid_if_qos_options_specified_but_no_qos_handler() + public void Configuration_is_invalid_if_qos_options_specified_but_no_qos_handler() { var route = GivenDefaultRoute("/laura", "/"); route.Key = "Laura"; route.QoSOptions = new FileQoSOptions - { + { TimeoutValue = 1, ExceptionsAllowedBeforeBreaking = 1, }; @@ -175,13 +175,13 @@ public void configuration_is_invalid_if_qos_options_specified_but_no_qos_handler } [Fact] - public void configuration_is_invalid_if_qos_options_specified_globally_but_no_qos_handler() + public void Configuration_is_invalid_if_qos_options_specified_globally_but_no_qos_handler() { var route = GivenDefaultRoute("/laura", "/"); route.Key = "Laura"; var configuration = GivenAConfiguration(route); configuration.GlobalConfiguration.QoSOptions = new FileQoSOptions - { + { TimeoutValue = 1, ExceptionsAllowedBeforeBreaking = 1, }; @@ -194,26 +194,22 @@ public void configuration_is_invalid_if_qos_options_specified_globally_but_no_qo } [Fact] - public void configuration_is_valid_if_aggregates_are_valid() + public void Configuration_is_valid_if_aggregates_are_valid() { var route = GivenDefaultRoute("/laura", "/"); route.Key = "Laura"; var route2 = GivenDefaultRoute("/tom", "/"); route2.Key = "Tom"; var configuration = GivenAConfiguration(route, route2); - configuration.Aggregates = new List + configuration.Aggregates = new() { new() { UpstreamPathTemplate = "/", UpstreamHost = "localhost", - RouteKeys = - [ - "Tom", - "Laura", - ], + RouteKeys = new() { "Tom", "Laura" }, }, - }; + }; this.Given(x => x.GivenAConfiguration(configuration)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsValid()) @@ -221,7 +217,7 @@ public void configuration_is_valid_if_aggregates_are_valid() } [Fact] - public void configuration_is_invalid_if_aggregates_are_duplicate_of_routes() + public void Configuration_is_invalid_if_aggregates_are_duplicate_of_routes() { var route = GivenDefaultRoute("/laura", "/"); route.Key = "Laura"; @@ -229,19 +225,15 @@ public void configuration_is_invalid_if_aggregates_are_duplicate_of_routes() route2.Key = "Tom"; route2.UpstreamHost = "localhost"; var configuration = GivenAConfiguration(route, route2); - configuration.Aggregates = new List + configuration.Aggregates = new() { new() { UpstreamPathTemplate = "/tom", UpstreamHost = "localhost", - RouteKeys = - [ - "Tom", - "Laura", - ], + RouteKeys = new() { "Tom", "Laura" }, }, - }; + }; this.Given(x => x.GivenAConfiguration(configuration)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) @@ -250,27 +242,23 @@ public void configuration_is_invalid_if_aggregates_are_duplicate_of_routes() } [Fact] - public void configuration_is_valid_if_aggregates_are_not_duplicate_of_routes() + public void Configuration_is_valid_if_aggregates_are_not_duplicate_of_routes() { var route = GivenDefaultRoute("/laura", "/"); route.Key = "Laura"; var route2 = GivenDefaultRoute("/tom", "/"); route2.Key = "Tom"; - route2.UpstreamHttpMethod = new List { "Post" }; + route2.UpstreamHttpMethod = new() { "Post" }; var configuration = GivenAConfiguration(route, route2); - configuration.Aggregates = new List + configuration.Aggregates = new() { new() { UpstreamPathTemplate = "/tom", UpstreamHost = "localhost", - RouteKeys = - [ - "Tom", - "Laura", - ], + RouteKeys = new() { "Tom", "Laura" }, }, - }; + }; this.Given(x => x.GivenAConfiguration(configuration)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsValid()) @@ -278,36 +266,28 @@ public void configuration_is_valid_if_aggregates_are_not_duplicate_of_routes() } [Fact] - public void configuration_is_invalid_if_aggregates_are_duplicate_of_aggregates() - { + public void Configuration_is_invalid_if_aggregates_are_duplicate_of_aggregates() + { var route = GivenDefaultRoute("/laura", "/"); route.Key = "Laura"; var route2 = GivenDefaultRoute("/lol", "/"); route2.Key = "Tom"; var configuration = GivenAConfiguration(route, route2); - configuration.Aggregates = new List + configuration.Aggregates = new() { new() { UpstreamPathTemplate = "/tom", UpstreamHost = "localhost", - RouteKeys = - [ - "Tom", - "Laura", - ], + RouteKeys = new() { "Tom", "Laura" }, }, new() { UpstreamPathTemplate = "/tom", UpstreamHost = "localhost", - RouteKeys = - [ - "Tom", - "Laura", - ], + RouteKeys = new() { "Tom", "Laura" }, }, - }; + }; this.Given(x => x.GivenAConfiguration(configuration)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) @@ -316,22 +296,18 @@ public void configuration_is_invalid_if_aggregates_are_duplicate_of_aggregates() } [Fact] - public void configuration_is_invalid_if_routes_dont_exist_for_aggregate() + public void Configuration_is_invalid_if_routes_dont_exist_for_aggregate() { var route = GivenDefaultRoute("/laura", "/"); route.Key = "Laura"; var configuration = GivenAConfiguration(route); - configuration.Aggregates = new List + configuration.Aggregates = new() { new() { UpstreamPathTemplate = "/", UpstreamHost = "localhost", - RouteKeys = - [ - "Tom", - "Laura", - ], + RouteKeys = new() { "Tom", "Laura" }, }, }; this.Given(x => x.GivenAConfiguration(configuration)) @@ -342,7 +318,7 @@ public void configuration_is_invalid_if_routes_dont_exist_for_aggregate() } [Fact] - public void configuration_is_invalid_if_aggregate_has_routes_with_specific_request_id_keys() + public void Configuration_is_invalid_if_aggregate_has_routes_with_specific_request_id_keys() { var route = GivenDefaultRoute("/laura", "/"); route.Key = "Laura"; @@ -350,19 +326,15 @@ public void configuration_is_invalid_if_aggregate_has_routes_with_specific_reque route2.Key = "Tom"; route2.RequestIdKey = "should_fail"; var configuration = GivenAConfiguration(route, route2); - configuration.Aggregates = new List + configuration.Aggregates = new() { new() { UpstreamPathTemplate = "/", UpstreamHost = "localhost", - RouteKeys = - [ - "Tom", - "Laura", - ], + RouteKeys = new() { "Tom", "Laura" }, }, - }; + }; this.Given(x => x.GivenAConfiguration(configuration)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) @@ -371,7 +343,7 @@ public void configuration_is_invalid_if_aggregate_has_routes_with_specific_reque } [Fact] - public void configuration_is_invalid_if_scheme_in_downstream_or_upstream_template() + public void Configuration_is_invalid_if_scheme_in_downstream_or_upstream_template() { this.Given(x => x.GivenAConfiguration(GivenDefaultRoute("http://asdf.com", "http://www.bbc.co.uk/api/products/{productId}"))) .When(x => x.WhenIValidateTheConfiguration()) @@ -388,7 +360,7 @@ public void configuration_is_invalid_if_scheme_in_downstream_or_upstream_templat } [Fact] - public void configuration_is_valid_with_one_route() + public void Configuration_is_valid_with_one_route() { this.Given(x => x.GivenAConfiguration(GivenDefaultRoute())) .When(x => x.WhenIValidateTheConfiguration()) @@ -397,7 +369,7 @@ public void configuration_is_valid_with_one_route() } [Fact] - public void configuration_is_invalid_without_slash_prefix_downstream_path_template() + public void Configuration_is_invalid_without_slash_prefix_downstream_path_template() { this.Given(x => x.GivenAConfiguration(GivenDefaultRoute("/asdf/", "api/products/"))) .When(x => x.WhenIValidateTheConfiguration()) @@ -407,8 +379,8 @@ public void configuration_is_invalid_without_slash_prefix_downstream_path_templa } [Fact] - public void configuration_is_invalid_without_slash_prefix_upstream_path_template() - { + public void Configuration_is_invalid_without_slash_prefix_upstream_path_template() + { this.Given(x => x.GivenAConfiguration(GivenDefaultRoute("api/prod/", "/api/products/"))) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) @@ -417,7 +389,7 @@ public void configuration_is_invalid_without_slash_prefix_upstream_path_template } [Fact] - public void configuration_is_invalid_if_upstream_url_contains_forward_slash_then_another_forward_slash() + public void Configuration_is_invalid_if_upstream_url_contains_forward_slash_then_another_forward_slash() { this.Given(x => x.GivenAConfiguration(GivenDefaultRoute("//api/prod/", "/api/products/"))) .When(x => x.WhenIValidateTheConfiguration()) @@ -427,7 +399,7 @@ public void configuration_is_invalid_if_upstream_url_contains_forward_slash_then } [Fact] - public void configuration_is_invalid_if_downstream_url_contains_forward_slash_then_another_forward_slash() + public void Configuration_is_invalid_if_downstream_url_contains_forward_slash_then_another_forward_slash() { this.Given(x => x.GivenAConfiguration(GivenDefaultRoute("/api/prod/", "//api/products/"))) .When(x => x.WhenIValidateTheConfiguration()) @@ -437,8 +409,8 @@ public void configuration_is_invalid_if_downstream_url_contains_forward_slash_th } [Fact] - public void configuration_is_valid_with_valid_authentication_provider() - { + public void Configuration_is_valid_with_valid_authentication_provider() + { var route = GivenDefaultRoute(); route.AuthenticationOptions.AuthenticationProviderKey = "Test"; this.Given(x => x.GivenAConfiguration(route)) @@ -449,13 +421,13 @@ public void configuration_is_valid_with_valid_authentication_provider() } [Fact] - public void configuration_is_invalid_with_invalid_authentication_provider() + public void Configuration_is_invalid_with_invalid_authentication_provider() { var route = GivenDefaultRoute(); route.AuthenticationOptions = new FileAuthenticationOptions() { AuthenticationProviderKey = "Test", - AuthenticationProviderKeys = new[] { "Test #1", "Test #2" }, + AuthenticationProviderKeys = new string[] { "Test #1", "Test #2" }, }; this.Given(x => x.GivenAConfiguration(route)) .When(x => x.WhenIValidateTheConfiguration()) @@ -465,8 +437,8 @@ public void configuration_is_invalid_with_invalid_authentication_provider() } [Fact] - public void configuration_is_not_valid_with_duplicate_routes_all_verbs() - { + public void Configuration_is_not_valid_with_duplicate_routes_all_verbs() + { var route = GivenDefaultRoute(); var duplicate = GivenDefaultRoute(); duplicate.DownstreamPathTemplate = "/www/test/"; @@ -478,7 +450,7 @@ public void configuration_is_not_valid_with_duplicate_routes_all_verbs() } [Fact] - public void configuration_is_valid_with_duplicate_routes_all_verbs_but_different_hosts() + public void Configuration_is_valid_with_duplicate_routes_all_verbs_but_different_hosts() { var route = GivenDefaultRoute(); route.UpstreamHost = "host1"; @@ -491,11 +463,11 @@ public void configuration_is_valid_with_duplicate_routes_all_verbs_but_different } [Fact] - public void configuration_is_not_valid_with_duplicate_routes_specific_verbs() + public void Configuration_is_not_valid_with_duplicate_routes_specific_verbs() { var route = GivenDefaultRoute(); var duplicate = GivenDefaultRoute(null, "/www/test/"); - duplicate.UpstreamHttpMethod = new List { "Get" }; + duplicate.UpstreamHttpMethod = new() { "Get" }; this.Given(x => x.GivenAConfiguration(route, duplicate)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) @@ -504,11 +476,11 @@ public void configuration_is_not_valid_with_duplicate_routes_specific_verbs() } [Fact] - public void configuration_is_valid_with_duplicate_routes_different_verbs() + public void Configuration_is_valid_with_duplicate_routes_different_verbs() { var route = GivenDefaultRoute(); // "Get" verb is inside var duplicate = GivenDefaultRoute(null, "/www/test/"); - duplicate.UpstreamHttpMethod = new List { "Post" }; + duplicate.UpstreamHttpMethod = new() { "Post" }; this.Given(x => x.GivenAConfiguration(route, duplicate)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsValid()) @@ -516,7 +488,7 @@ public void configuration_is_valid_with_duplicate_routes_different_verbs() } [Fact] - public void configuration_is_not_valid_with_duplicate_routes_with_duplicated_upstreamhosts() + public void Configuration_is_not_valid_with_duplicate_routes_with_duplicated_upstreamhosts() { var route = GivenDefaultRoute(); route.UpstreamHttpMethod = new(); @@ -534,7 +506,7 @@ public void configuration_is_not_valid_with_duplicate_routes_with_duplicated_ups } [Fact] - public void configuration_is_valid_with_duplicate_routes_but_different_upstreamhosts() + public void Configuration_is_valid_with_duplicate_routes_but_different_upstreamhosts() { var route = GivenDefaultRoute(); route.UpstreamHttpMethod = new(); @@ -551,7 +523,7 @@ public void configuration_is_valid_with_duplicate_routes_but_different_upstreamh } [Fact] - public void configuration_is_valid_with_duplicate_routes_but_one_upstreamhost_is_not_set() + public void Configuration_is_valid_with_duplicate_routes_but_one_upstreamhost_is_not_set() { var route = GivenDefaultRoute(); route.UpstreamHttpMethod = new(); @@ -567,7 +539,7 @@ public void configuration_is_valid_with_duplicate_routes_but_one_upstreamhost_is } [Fact] - public void configuration_is_invalid_with_invalid_rate_limit_configuration() + public void Configuration_is_invalid_with_invalid_rate_limit_configuration() { var route = GivenDefaultRoute(); route.RateLimitOptions = new FileRateLimitRule @@ -583,7 +555,7 @@ public void configuration_is_invalid_with_invalid_rate_limit_configuration() } [Fact] - public void configuration_is_valid_with_valid_rate_limit_configuration() + public void Configuration_is_valid_with_valid_rate_limit_configuration() { var route = GivenDefaultRoute(); route.RateLimitOptions = new FileRateLimitRule @@ -598,7 +570,7 @@ public void configuration_is_valid_with_valid_rate_limit_configuration() } [Fact] - public void configuration_is_valid_with_using_service_discovery_and_service_name() + public void Configuration_is_valid_with_using_service_discovery_and_service_name() { var route = GivenServiceDiscoveryRoute(); var config = GivenAConfiguration(route); @@ -608,14 +580,14 @@ public void configuration_is_valid_with_using_service_discovery_and_service_name .Then(x => x.ThenTheResultIsValid()) .BDDfy(); } - + private const string Empty = ""; [Theory] [InlineData(null)] [InlineData(Empty)] - public void configuration_is_invalid_when_not_using_service_discovery_and_host(string downstreamHost) - { + public void Configuration_is_invalid_when_not_using_service_discovery_and_host(string downstreamHost) + { var route = GivenDefaultRoute(); route.DownstreamHostAndPorts[0].Host = downstreamHost; this.Given(x => x.GivenAConfiguration(route)) @@ -623,8 +595,8 @@ public void configuration_is_invalid_when_not_using_service_discovery_and_host(s .Then(x => x.ThenTheResultIsNotValid()) .And(x => x.ThenTheErrorMessageAtPositionIs(0, "When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using Route.Host or Ocelot cannot find your service!")) .BDDfy(); - } - + } + [Theory] [InlineData(null, true)] [InlineData(Empty, true)] @@ -651,14 +623,14 @@ public void HaveServiceDiscoveryProviderRegistered_RouteServiceName_Validated(st [InlineData(true, "type", false)] [InlineData(true, "servicefabric", true)] public void HaveServiceDiscoveryProviderRegistered_ServiceDiscoveryProvider_Validated(bool create, string type, bool valid) - { + { // Arrange var route = GivenServiceDiscoveryRoute(); var config = GivenAConfiguration(route); var provider = create ? GivenDefaultServiceDiscoveryProvider() : null; config.GlobalConfiguration.ServiceDiscoveryProvider = provider; if (create && provider != null) - { + { provider.Type = type; } @@ -694,10 +666,10 @@ public void HaveServiceDiscoveryProviderRegistered_ServiceDiscoveryFinderDelegat } [Fact] - public void configuration_is_valid_when_not_using_service_discovery_and_host_is_set() + public void Configuration_is_valid_when_not_using_service_discovery_and_host_is_set() { var route = GivenDefaultRoute(); - route.DownstreamHostAndPorts = new List + route.DownstreamHostAndPorts = new() { new("bbc.co.uk", 123), }; @@ -708,10 +680,10 @@ public void configuration_is_valid_when_not_using_service_discovery_and_host_is_ } [Fact] - public void configuration_is_valid_when_no_downstream_but_has_host_and_port() + public void Configuration_is_valid_when_no_downstream_but_has_host_and_port() { var route = GivenDefaultRoute(); - route.DownstreamHostAndPorts = new List + route.DownstreamHostAndPorts = new() { new("test", 123), }; @@ -722,7 +694,7 @@ public void configuration_is_valid_when_no_downstream_but_has_host_and_port() } [Fact] - public void configuration_is_not_valid_when_no_host_and_port() + public void Configuration_is_not_valid_when_no_host_and_port() { var route = GivenDefaultRoute(); route.DownstreamHostAndPorts = new(); @@ -734,10 +706,10 @@ public void configuration_is_not_valid_when_no_host_and_port() } [Fact] - public void configuration_is_not_valid_when_host_and_port_is_empty() + public void Configuration_is_not_valid_when_host_and_port_is_empty() { var route = GivenDefaultRoute(); - route.DownstreamHostAndPorts = new List + route.DownstreamHostAndPorts = new() { new(), }; @@ -747,44 +719,94 @@ public void configuration_is_not_valid_when_host_and_port_is_empty() .And(x => x.ThenTheErrorMessageAtPositionIs(0, "When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using Route.Host or Ocelot cannot find your service!")) .BDDfy(); } - - [Fact] - public void configuration_is_invalid_when_placeholder_is_used_twice_in_upstream_path_template() + + [Theory] + [Trait("PR", "1927")] + [InlineData("/foo/{bar}/foo", "/yahoo/foo/{bar}")] // valid + [InlineData("/foo/{bar}/{foo}", "/yahoo/{foo}/{bar}")] // valid + [InlineData("/foo/{bar}/{bar}", "/yahoo/foo/{bar}", "UpstreamPathTemplate '/foo/{bar}/{bar}' has duplicated placeholder")] // invalid + [InlineData("/foo/{bar}/{bar}", "/yahoo/{foo}/{bar}", "UpstreamPathTemplate '/foo/{bar}/{bar}' has duplicated placeholder")] // invalid + [InlineData("/yahoo/foo/{bar}", "/foo/{bar}/foo")] // valid + [InlineData("/yahoo/{foo}/{bar}", "/foo/{bar}/{foo}")] // valid + [InlineData("/yahoo/foo/{bar}", "/foo/{bar}/{bar}", "DownstreamPathTemplate '/foo/{bar}/{bar}' has duplicated placeholder")] // invalid + [InlineData("/yahoo/{foo}/{bar}", "/foo/{bar}/{bar}", "DownstreamPathTemplate '/foo/{bar}/{bar}' has duplicated placeholder")] // invalid + public void IsPlaceholderNotDuplicatedIn_RuleForFileRoute_PathTemplatePlaceholdersAreValidated(string upstream, string downstream, params string[] expected) { - var route = GivenDefaultRoute("/foo/bar/{everything}/{everything}", "/bar/{everything}"); - this.Given(x => x.GivenAConfiguration(route)) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "route /foo/bar/{everything}/{everything} has duplicated placeholder")) - .BDDfy(); - } + // Arrange + var route = GivenDefaultRoute(upstream, downstream); + GivenAConfiguration(route); + + // Act + WhenIValidateTheConfiguration(); + + // Assert + ThenThereAreErrors(expected.Length > 0); + ThenTheErrorMessagesAre(expected); + } + + [Theory] + [Trait("PR", "1927")] + [Trait("Bug", "683")] + [InlineData("/foo/bar/{everything}/{everything}", "/bar/{everything}", "foo", "UpstreamPathTemplate '/foo/bar/{everything}/{everything}' has duplicated placeholder")] + [InlineData("/foo/bar/{everything}/{everything}", "/bar/{everything}/{everything}", "foo", "UpstreamPathTemplate '/foo/bar/{everything}/{everything}' has duplicated placeholder", "DownstreamPathTemplate '/bar/{everything}/{everything}' has duplicated placeholder")] + public void Configuration_is_invalid_when_placeholder_is_used_twice_in_upstream_path_template(string upstream, string downstream, string host, params string[] expected) + { + // Arrange + var route = GivenDefaultRoute(upstream, downstream, host); + GivenAConfiguration(route); + + // Act + WhenIValidateTheConfiguration(); + + // Assert + ThenTheResultIsNotValid(); + ThenTheErrorMessagesAre(expected); + } + + [Theory] + [Trait("PR", "1927")] + [Trait("Bug", "683")] + [InlineData("/foo/bar/{everything}", "/bar/{everything}/{everything}", "foo", "DownstreamPathTemplate '/bar/{everything}/{everything}' has duplicated placeholder")] + [InlineData("/foo/bar/{everything}/{everything}", "/bar/{everything}/{everything}", "foo", "UpstreamPathTemplate '/foo/bar/{everything}/{everything}' has duplicated placeholder", "DownstreamPathTemplate '/bar/{everything}/{everything}' has duplicated placeholder")] + public void Configuration_is_invalid_when_placeholder_is_used_twice_in_downstream_path_template(string upstream, string downstream, string host, params string[] expected) + { + // Arrange + var route = GivenDefaultRoute(upstream, downstream, host); + GivenAConfiguration(route); + + // Act + WhenIValidateTheConfiguration(); - private FileRoute GivenDefaultRoute() => GivenDefaultRoute(null, null); + // Assert + ThenTheResultIsNotValid(); + ThenTheErrorMessagesAre(expected); + } + + private static FileRoute GivenDefaultRoute() => GivenDefaultRoute(null, null, null); + private static FileRoute GivenDefaultRoute(string upstream, string downstream) => GivenDefaultRoute(upstream, downstream, null); - private FileRoute GivenDefaultRoute(string upstreamPathTemplate, string downstreamPathTemplate) => new() + private static FileRoute GivenDefaultRoute(string upstream, string downstream, string host) => new() { - DownstreamPathTemplate = downstreamPathTemplate ?? "/api/products/", - UpstreamPathTemplate = upstreamPathTemplate ?? "/asdf/", - UpstreamHttpMethod = new List { "Get" }, - DownstreamHostAndPorts = new List + UpstreamHttpMethod = new() { HttpMethods.Get }, + UpstreamPathTemplate = upstream ?? "/asdf/", + DownstreamPathTemplate = downstream ?? "/api/products/", + DownstreamHostAndPorts = new() { - new("bbc.co.uk", 12345), + new(host ?? "bbc.co.uk", 12345), }, + DownstreamScheme = Uri.UriSchemeHttp, }; - private FileRoute GivenServiceDiscoveryRoute() => new() + private static FileRoute GivenServiceDiscoveryRoute() => new() { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", + UpstreamHttpMethod = new() { HttpMethods.Get }, UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, + DownstreamPathTemplate = "/", + DownstreamScheme = Uri.UriSchemeHttp, ServiceName = "test", }; - private void GivenAConfiguration(FileConfiguration fileConfiguration) - { - _fileConfiguration = fileConfiguration; - } + private void GivenAConfiguration(FileConfiguration fileConfiguration) => _fileConfiguration = fileConfiguration; private FileConfiguration GivenAConfiguration(params FileRoute[] routes) { @@ -793,10 +815,10 @@ private FileConfiguration GivenAConfiguration(params FileRoute[] routes) _fileConfiguration = config; return config; } - - private FileServiceDiscoveryProvider GivenDefaultServiceDiscoveryProvider() => new FileServiceDiscoveryProvider + + private static FileServiceDiscoveryProvider GivenDefaultServiceDiscoveryProvider() => new() { - Scheme = "https", + Scheme = Uri.UriSchemeHttps, Host = "localhost", Type = "ServiceFabric", Port = 8500, @@ -825,6 +847,21 @@ private void ThenTheErrorIs() private void ThenTheErrorMessageAtPositionIs(int index, string expected) { _result.Data.Errors[index].Message.ShouldBe(expected); + } + + private void ThenThereAreErrors(bool isError) + { + _result.Data.IsError.ShouldBe(isError); + } + + private void ThenTheErrorMessagesAre(IEnumerable messages) + { + _result.Data.Errors.Count.ShouldBe(messages.Count()); + + foreach (var msg in messages) + { + _result.Data.Errors.ShouldContain(e => e.Message == msg); + } } private void GivenTheAuthSchemeExists(string name) diff --git a/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs index 69a7401e9c..84d041e23d 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Configuration.Validation { - public class FileQoSOptionsFluentValidatorTests + public class FileQoSOptionsFluentValidatorTests : UnitTest { private FileQoSOptionsFluentValidator _validator; private readonly ServiceCollection _services; diff --git a/test/Ocelot.UnitTests/Configuration/Validation/HostAndPortValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/HostAndPortValidatorTests.cs index 9ea2bb3642..af975654f8 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/HostAndPortValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/HostAndPortValidatorTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Configuration.Validation { - public class HostAndPortValidatorTests + public class HostAndPortValidatorTests : UnitTest { private HostAndPortValidator _validator; private ValidationResult _result; diff --git a/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs index 2e696f41d4..1b2bfadb37 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.Configuration.Validation { - public class RouteFluentValidatorTests + public class RouteFluentValidatorTests : UnitTest { private readonly RouteFluentValidator _validator; private readonly Mock _authProvider; diff --git a/test/Ocelot.UnitTests/Configuration/VersionCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/VersionCreatorTests.cs index dde40682ad..129ff791ae 100644 --- a/test/Ocelot.UnitTests/Configuration/VersionCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/VersionCreatorTests.cs @@ -2,7 +2,7 @@ namespace Ocelot.UnitTests.Configuration { - public class VersionCreatorTests + public class VersionCreatorTests : UnitTest { private readonly HttpVersionCreator _creator; private string _input; diff --git a/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs index 64dd4e9564..afdf95ea7c 100644 --- a/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Consul { - public class ConsulFileConfigurationRepositoryTests + public class ConsulFileConfigurationRepositoryTests : UnitTest { private ConsulFileConfigurationRepository _repo; private readonly Mock> _options; diff --git a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs index bf570142c3..7df0a2c603 100644 --- a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Consul { - public class ConsulServiceDiscoveryProviderTests : IDisposable + public class ConsulServiceDiscoveryProviderTests : UnitTest, IDisposable { private IWebHost _fakeConsulBuilder; private readonly List _serviceEntries; diff --git a/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs index f324f4b4d3..b9c7532c03 100644 --- a/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.Consul { - public class OcelotBuilderExtensionsTests + public class OcelotBuilderExtensionsTests : UnitTest { private readonly IServiceCollection _services; private readonly IConfiguration _configRoot; diff --git a/test/Ocelot.UnitTests/Consul/PollingConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Consul/PollingConsulServiceDiscoveryProviderTests.cs index a5dc995853..5b8eda527c 100644 --- a/test/Ocelot.UnitTests/Consul/PollingConsulServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/Consul/PollingConsulServiceDiscoveryProviderTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.Consul { - public class PollingConsulServiceDiscoveryProviderTests + public class PollingConsulServiceDiscoveryProviderTests : UnitTest { private readonly int _delay; private readonly List _services; diff --git a/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs b/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs index 6d4be09a95..656ea340ad 100644 --- a/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs +++ b/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.Controllers { - public class FileConfigurationControllerTests + public class FileConfigurationControllerTests : UnitTest { private readonly FileConfigurationController _controller; private readonly Mock _repo; diff --git a/test/Ocelot.UnitTests/Controllers/OutputCacheControllerTests.cs b/test/Ocelot.UnitTests/Controllers/OutputCacheControllerTests.cs index 067d06c683..71c2b4fa61 100644 --- a/test/Ocelot.UnitTests/Controllers/OutputCacheControllerTests.cs +++ b/test/Ocelot.UnitTests/Controllers/OutputCacheControllerTests.cs @@ -3,7 +3,7 @@ namespace Ocelot.UnitTests.Controllers { - public class OutputCacheControllerTests + public class OutputCacheControllerTests : UnitTest { private readonly OutputCacheController _controller; private readonly Mock> _cache; diff --git a/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs index 80a00bb1ad..f0e322fa87 100644 --- a/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs @@ -1,15 +1,16 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Newtonsoft.Json; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; - +using System.Runtime.CompilerServices; + namespace Ocelot.UnitTests.DependencyInjection { - public class ConfigurationBuilderExtensionsTests + public sealed class ConfigurationBuilderExtensionsTests : FileUnitTest { private IConfigurationRoot _configuration; - private string _result; private IConfigurationRoot _configRoot; private FileConfiguration _globalConfig; private FileConfiguration _routeA; @@ -17,78 +18,129 @@ public class ConfigurationBuilderExtensionsTests private FileConfiguration _aggregate; private FileConfiguration _envSpecific; private FileConfiguration _combinedFileConfiguration; - private readonly Mock _hostingEnvironment; + private readonly Mock _hostingEnvironment; public ConfigurationBuilderExtensionsTests() { - _hostingEnvironment = new Mock(); - - // Clean up config files before each test - var subConfigFiles = new DirectoryInfo(".").GetFiles("ocelot.*.json"); - - foreach (var config in subConfigFiles) - { - config.Delete(); - } - } - + _hostingEnvironment = new Mock(); + } + + protected override string EnvironmentName() + => _hostingEnvironment?.Object?.EnvironmentName ?? base.EnvironmentName(); + [Fact] public void Should_add_base_url_to_config() - { - this.Given(_ => GivenTheBaseUrl("test")) - .When(_ => WhenIGet("BaseUrl")) - .Then(_ => ThenTheResultIs("test")) - .BDDfy(); + { + // Arrange + _configuration = new ConfigurationBuilder() + .AddOcelotBaseUrl("test") + .Build(); + + // Act + var actual = _configuration.GetValue("BaseUrl", string.Empty); + + // Assert + actual.ShouldBe("test"); } - [Fact] - public void Should_merge_files() - { - this.Given(_ => GivenMultipleConfigurationFiles(string.Empty, false)) - .And(_ => GivenTheEnvironmentIs(null)) - .When(_ => WhenIAddOcelotConfiguration()) - .Then(_ => ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false)) - .BDDfy(); + [Fact] + [Trait("PR", "1227")] + [Trait("Issue", "1216")] + public void Should_merge_files_to_file() + { + // Arrange + GivenTheEnvironmentIs(TestID); + GivenMultipleConfigurationFiles(TestID); + + // Act + WhenIAddOcelotConfiguration(TestID); + + // Assert + ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false); + TheOcelotPrimaryConfigFileExists(true); } [Fact] public void Should_store_given_configurations_when_provided_file_configuration_object() - { - this.Given(_ => GivenCombinedFileConfigurationObject(string.Empty)) - .And(_ => GivenTheEnvironmentIs(null)) - .When(_ => WhenIAddOcelotConfigurationWithCombinedFileConfiguration()) - .Then(_ => ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(true)) - .BDDfy(); + { + // Arrange + GivenTheEnvironmentIs(TestID); + GivenCombinedFileConfigurationObject(); + + // Act + WhenIAddOcelotConfigurationWithCombinedFileConfiguration(); + + // Assert + ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(true); } [Fact] public void Should_merge_files_except_env() { - this.Given(_ => GivenMultipleConfigurationFiles(string.Empty, true)) - .And(_ => GivenTheEnvironmentIs("Env")) - .When(_ => WhenIAddOcelotConfiguration()) - .Then(_ => ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false)) - .And(_ => NotContainsEnvSpecificConfig()) - .BDDfy(); + // Arrange + GivenTheEnvironmentIs(TestID); + GivenMultipleConfigurationFiles(TestID, true); + + // Act + WhenIAddOcelotConfiguration(TestID); + + // Assert + ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false); + NotContainsEnvSpecificConfig(); } [Fact] public void Should_merge_files_in_specific_folder() { - var configFolder = "ConfigFiles"; - this.Given(_ => GivenMultipleConfigurationFiles(configFolder, false)) - .When(_ => WhenIAddOcelotConfigurationWithSpecificFolder(configFolder)) - .Then(_ => ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false)) - .BDDfy(); + // Arrange + GivenMultipleConfigurationFiles(TestID); + + // Act + WhenIAddOcelotConfiguration(TestID); + + // Assert + ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false); } - private void GivenCombinedFileConfigurationObject(string folder) - { - if (!string.IsNullOrEmpty(folder)) - { - Directory.CreateDirectory(folder); - } + [Fact] + [Trait("PR", "1227")] + [Trait("Issue", "1216")] + public void Should_merge_files_to_memory() + { + // Arrange + GivenTheEnvironmentIs(TestID); + GivenMultipleConfigurationFiles(TestID); + + // Act + WhenIAddOcelotConfiguration(TestID, MergeOcelotJson.ToMemory); + + // Assert + ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false); + TheOcelotPrimaryConfigFileExists(false); + } + + [Fact] + [Trait("PR", "1986")] + [Trait("Issue", "1518")] + public void Should_merge_files_with_null_environment() + { + // Arrange + _environmentConfigFileName = null; // Ups! + const IWebHostEnvironment NullEnvironment = null; // Wow! + GivenMultipleConfigurationFiles(TestID, false); + + // Act + _configRoot = new ConfigurationBuilder() + .AddOcelot(TestID, NullEnvironment, MergeOcelotJson.ToMemory, _primaryConfigFileName, _globalConfigFileName, _environmentConfigFileName, false, false) + .Build(); + + // Assert + ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false); + TheOcelotPrimaryConfigFileExists(false); + } + private void GivenCombinedFileConfigurationObject() + { _combinedFileConfiguration = new FileConfiguration { GlobalConfiguration = GetFileGlobalConfigurationData(), @@ -97,235 +149,107 @@ private void GivenCombinedFileConfigurationObject(string folder) }; } - private void GivenMultipleConfigurationFiles(string folder, bool addEnvSpecificConfig) + private void GivenMultipleConfigurationFiles(string folder, bool withEnvironment = false) { - if (!string.IsNullOrEmpty(folder)) - { - Directory.CreateDirectory(folder); - } - - _globalConfig = new FileConfiguration - { - GlobalConfiguration = GetFileGlobalConfigurationData(), - }; - - _routeA = new FileConfiguration - { - Routes = GetServiceARoutes(), - }; - - _routeB = new FileConfiguration - { - Routes = GetServiceBRoutes(), - }; - - _aggregate = new FileConfiguration - { - Aggregates = GetFileAggregatesRouteData(), - }; - - _envSpecific = new FileConfiguration - { - Routes = GetEnvironmentSpecificRoutes(), + _globalConfig = new() { GlobalConfiguration = GetFileGlobalConfigurationData() }; + _routeA = new() { Routes = GetServiceARoutes() }; + _routeB = new() { Routes = GetServiceBRoutes() }; + _aggregate = new() { Aggregates = GetFileAggregatesRouteData() }; + _envSpecific = new() { Routes = GetEnvironmentSpecificRoutes() }; + + var configParts = new Dictionary + { + { "global", _globalConfig }, + { "routesA", _routeA }, + { "routesB", _routeB }, + { "aggregates", _aggregate }, }; - - var globalFilename = Path.Combine(folder, "ocelot.global.json"); - var routesAFilename = Path.Combine(folder, "ocelot.routesA.json"); - var routesBFilename = Path.Combine(folder, "ocelot.routesB.json"); - var aggregatesFilename = Path.Combine(folder, "ocelot.aggregates.json"); - - File.WriteAllText(globalFilename, JsonConvert.SerializeObject(_globalConfig)); - File.WriteAllText(routesAFilename, JsonConvert.SerializeObject(_routeA)); - File.WriteAllText(routesBFilename, JsonConvert.SerializeObject(_routeB)); - File.WriteAllText(aggregatesFilename, JsonConvert.SerializeObject(_aggregate)); - - if (addEnvSpecificConfig) - { - var envSpecificFilename = Path.Combine(folder, "ocelot.Env.json"); - File.WriteAllText(envSpecificFilename, JsonConvert.SerializeObject(_envSpecific)); + + if (withEnvironment) + { + configParts.Add(EnvironmentName(), _envSpecific); } - } - private static FileGlobalConfiguration GetFileGlobalConfigurationData() - { - return new FileGlobalConfiguration + foreach (var part in configParts) { - BaseUrl = "BaseUrl", - RateLimitOptions = new FileRateLimitOptions - { - HttpStatusCode = 500, - ClientIdHeader = "ClientIdHeader", - DisableRateLimitHeaders = true, - QuotaExceededMessage = "QuotaExceededMessage", - RateLimitCounterPrefix = "RateLimitCounterPrefix", - }, - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "https", - Host = "Host", - Port = 80, - Type = "Type", - }, - RequestIdKey = "RequestIdKey", - }; + var filename = Path.Combine(folder, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, part.Key)); + File.WriteAllText(filename, JsonConvert.SerializeObject(part.Value, Formatting.Indented)); + _files.Add(filename); + } } - private static List GetFileAggregatesRouteData() + private static FileGlobalConfiguration GetFileGlobalConfigurationData() => new() { - return new List + BaseUrl = "BaseUrl", + RateLimitOptions = new() + { + HttpStatusCode = 500, + ClientIdHeader = "ClientIdHeader", + DisableRateLimitHeaders = true, + QuotaExceededMessage = "QuotaExceededMessage", + RateLimitCounterPrefix = "RateLimitCounterPrefix", + }, + ServiceDiscoveryProvider = new() + { + Scheme = "https", + Host = "Host", + Port = 80, + Type = "Type", + }, + RequestIdKey = "RequestIdKey", + }; + + private static List GetFileAggregatesRouteData() => new() + { + new() { - new() - { - RouteKeys = new List - { - "KeyB", - "KeyBB", - }, - UpstreamPathTemplate = "UpstreamPathTemplate", - }, - new() - { - RouteKeys = new List - { - "KeyB", - "KeyBB", - }, - UpstreamPathTemplate = "UpstreamPathTemplate", - }, - }; - } - - private static List GetServiceARoutes() + RouteKeys = new() { "KeyB", "KeyBB" }, + UpstreamPathTemplate = "UpstreamPathTemplate", + }, + }; + + private static FileRoute GetRoute(string suffix) => new() { - return new List - { - new() - { - DownstreamScheme = "DownstreamScheme", - DownstreamPathTemplate = "DownstreamPathTemplate", - Key = "Key", - UpstreamHost = "UpstreamHost", - UpstreamHttpMethod = new List - { - "UpstreamHttpMethod", - }, - DownstreamHostAndPorts = new List - { - new() - { - Host = "Host", - Port = 80, - }, - }, - }, - }; - } + DownstreamScheme = "DownstreamScheme" + suffix, + DownstreamPathTemplate = "DownstreamPathTemplate" + suffix, + Key = "Key" + suffix, + UpstreamHost = "UpstreamHost" + suffix, + UpstreamHttpMethod = new() { "UpstreamHttpMethod" + suffix }, + DownstreamHostAndPorts = new() + { + new("Host"+suffix, 80), + }, + }; - private static List GetServiceBRoutes() - { - return new List - { - new() - { - DownstreamScheme = "DownstreamSchemeB", - DownstreamPathTemplate = "DownstreamPathTemplateB", - Key = "KeyB", - UpstreamHost = "UpstreamHostB", - UpstreamHttpMethod = new List - { - "UpstreamHttpMethodB", - }, - DownstreamHostAndPorts = new List - { - new() - { - Host = "HostB", - Port = 80, - }, - }, - }, - new() - { - DownstreamScheme = "DownstreamSchemeBB", - DownstreamPathTemplate = "DownstreamPathTemplateBB", - Key = "KeyBB", - UpstreamHost = "UpstreamHostBB", - UpstreamHttpMethod = new List - { - "UpstreamHttpMethodBB", - }, - DownstreamHostAndPorts = new List - { - new() - { - Host = "HostBB", - Port = 80, - }, - }, - }, - }; - } + private static List GetServiceARoutes() => new() { GetRoute("A") }; + private static List GetServiceBRoutes() => new() { GetRoute("B"), GetRoute("BB") }; + private static List GetEnvironmentSpecificRoutes() => new() { GetRoute("Spec") }; - private static List GetEnvironmentSpecificRoutes() + private void GivenTheEnvironmentIs(string folder, [CallerMemberName] string testName = null) { - return new List - { - new() - { - DownstreamScheme = "DownstreamSchemeSpec", - DownstreamPathTemplate = "DownstreamPathTemplateSpec", - Key = "KeySpec", - UpstreamHost = "UpstreamHostSpec", - UpstreamHttpMethod = new List - { - "UpstreamHttpMethodSpec", - }, - DownstreamHostAndPorts = new List - { - new() - { - Host = "HostSpec", - Port = 80, - }, - }, - }, - }; - } - - private void GivenTheEnvironmentIs(string env) - { - _hostingEnvironment.SetupGet(x => x.EnvironmentName).Returns(env); - } - - private void WhenIAddOcelotConfiguration() - { - IConfigurationBuilder builder = new ConfigurationBuilder(); - - builder.AddOcelot(_hostingEnvironment.Object); - - _configRoot = builder.Build(); + _hostingEnvironment.SetupGet(x => x.EnvironmentName).Returns(testName); + _environmentConfigFileName = Path.Combine(folder, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, testName)); + _files.Add(_environmentConfigFileName); } private void WhenIAddOcelotConfigurationWithCombinedFileConfiguration() { - IConfigurationBuilder builder = new ConfigurationBuilder(); - - builder.AddOcelot(_combinedFileConfiguration); - - _configRoot = builder.Build(); + _configRoot = new ConfigurationBuilder() + .AddOcelot(_combinedFileConfiguration, _primaryConfigFileName, false, false) + .Build(); } - private void WhenIAddOcelotConfigurationWithSpecificFolder(string folder) + private void WhenIAddOcelotConfiguration(string folder, MergeOcelotJson mergeOcelotJson = MergeOcelotJson.ToFile) { - IConfigurationBuilder builder = new ConfigurationBuilder(); - builder.AddOcelot(folder, _hostingEnvironment.Object); - _configRoot = builder.Build(); + _configRoot = new ConfigurationBuilder() + .AddOcelot(folder, _hostingEnvironment.Object, mergeOcelotJson, _primaryConfigFileName, _globalConfigFileName, _environmentConfigFileName, false, false) + .Build(); } private void ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(bool useCombinedConfig) { var fc = (FileConfiguration)_configRoot.Get(typeof(FileConfiguration)); - + fc.GlobalConfiguration.BaseUrl.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.BaseUrl : _globalConfig.GlobalConfiguration.BaseUrl); fc.GlobalConfiguration.RateLimitOptions.ClientIdHeader.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.RateLimitOptions.ClientIdHeader : _globalConfig.GlobalConfiguration.RateLimitOptions.ClientIdHeader); fc.GlobalConfiguration.RateLimitOptions.DisableRateLimitHeaders.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.RateLimitOptions.DisableRateLimitHeaders : _globalConfig.GlobalConfiguration.RateLimitOptions.DisableRateLimitHeaders); @@ -366,24 +290,5 @@ private void NotContainsEnvSpecificConfig() fc.Routes.ShouldNotContain(x => x.DownstreamPathTemplate == _envSpecific.Routes[0].DownstreamPathTemplate); fc.Routes.ShouldNotContain(x => x.Key == _envSpecific.Routes[0].Key); } - - private void GivenTheBaseUrl(string baseUrl) - { -#pragma warning disable CS0618 - var builder = new ConfigurationBuilder() - .AddOcelotBaseUrl(baseUrl); -#pragma warning restore CS0618 - _configuration = builder.Build(); - } - - private void WhenIGet(string key) - { - _result = _configuration.GetValue(key, string.Empty); - } - - private void ThenTheResultIs(string expected) - { - _result.ShouldBe(expected); - } } } diff --git a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs index 8edd8c3548..2620149277 100644 --- a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs +++ b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs @@ -12,7 +12,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Moq; using Ocelot.Configuration.Setter; using Ocelot.DependencyInjection; using Ocelot.Infrastructure; @@ -28,11 +27,11 @@ namespace Ocelot.UnitTests.DependencyInjection { - public class OcelotBuilderTests + public class OcelotBuilderTests : UnitTest { + private readonly IConfiguration _configRoot; private readonly IServiceCollection _services; private IServiceProvider _serviceProvider; - private readonly IConfiguration _configRoot; private IOcelotBuilder _ocelotBuilder; private Exception _ex; @@ -273,11 +272,31 @@ private void CstorShouldUseDefaultBuilderToInitMvcCoreBuilder() } [Fact] - public void Should_use_custom_mvc_builder() + public void Should_use_custom_mvc_builder_no_configuration() { - this.Given(x => x.WhenISetupOcelotServicesWithCustomMvcBuider()) - .Then(x => CstorShouldUseCustomBuilderToInitMvcCoreBuilder()) - .BDDfy(); + // Arrange, Act + WhenISetupOcelotServicesWithCustomMvcBuider(); + + // Assert + CstorShouldUseCustomBuilderToInitMvcCoreBuilder(); + ShouldFindConfiguration(); + } + + [Theory] + [Trait("PR", "1986")] + [Trait("Issue", "1518")] + [InlineData(false)] + [InlineData(true)] + public void Should_use_custom_mvc_builder_with_configuration(bool hasConfig) + { + // Arrange, Act + WhenISetupOcelotServicesWithCustomMvcBuider( + hasConfig ? _configRoot : null, + true); + + // Assert + CstorShouldUseCustomBuilderToInitMvcCoreBuilder(); + ShouldFindConfiguration(); } private bool _fakeCustomBuilderCalled; @@ -293,12 +312,14 @@ private IMvcCoreBuilder FakeCustomBuilder(IMvcCoreBuilder builder, Assembly asse }); } - private void WhenISetupOcelotServicesWithCustomMvcBuider() + private void WhenISetupOcelotServicesWithCustomMvcBuider(IConfiguration configuration = null, bool useConfigParam = false) { _fakeCustomBuilderCalled = false; try { - _ocelotBuilder = _services.AddOcelotUsingBuilder(FakeCustomBuilder); + _ocelotBuilder = !useConfigParam + ? _services.AddOcelotUsingBuilder(FakeCustomBuilder) + : _services.AddOcelotUsingBuilder(configuration, FakeCustomBuilder); } catch (Exception e) { @@ -326,6 +347,14 @@ private void CstorShouldUseCustomBuilderToInitMvcCoreBuilder() .ShouldNotBeNull().ShouldBeOfType>(); } + private void ShouldFindConfiguration() + { + _ocelotBuilder.ShouldNotBeNull(); + var actual = _ocelotBuilder.Configuration.ShouldNotBeNull(); + actual.Equals(_configRoot).ShouldBeTrue(); // check references equality + actual.ShouldBe(_configRoot); + } + private void AddSingletonDefinedAggregator() where T : class, IDefinedAggregator { diff --git a/test/Ocelot.UnitTests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/test/Ocelot.UnitTests/DependencyInjection/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000000..87a7c97d92 --- /dev/null +++ b/test/Ocelot.UnitTests/DependencyInjection/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,109 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Ocelot.DependencyInjection; +using System.Reflection; +using Extensions = Ocelot.DependencyInjection.ServiceCollectionExtensions; + +namespace Ocelot.UnitTests.DependencyInjection; + +public class ServiceCollectionExtensionsTests +{ + [Fact] + [Trait("PR", "1986")] + [Trait("Issue", "1518")] + public void AddOcelot_NoConfiguration_DefaultConfiguration() + { + // Arrange + var services = new ServiceCollection(); + + // Act + var ocelot = services.AddOcelot(); + + // Assert + ocelot.ShouldNotBeNull() + .Configuration.ShouldNotBeNull(); + } + + [Theory] + [Trait("PR", "1986")] + [Trait("Issue", "1518")] + [InlineData(false)] + [InlineData(true)] + public void FindConfiguration_HasDescriptor_HappyPath(bool hasConfig) + { + // Arrange + IConfiguration config = hasConfig ? new ConfigurationBuilder().Build() : null; + var descriptor = new ServiceDescriptor(typeof(IConfiguration), (p) => config, ServiceLifetime.Transient); + var services = new ServiceCollection().Add(descriptor); + IWebHostEnvironment env = null; + + // Act + var method = typeof(Extensions).GetMethod("FindConfiguration", BindingFlags.NonPublic | BindingFlags.Static); + var actual = (IConfiguration)method.Invoke(null, new object[] { services, env }); + + // Assert + actual.ShouldNotBeNull(); + if (hasConfig) + { + actual.Equals(config).ShouldBeTrue(); + } + } + + [Fact] + [Trait("PR", "1986")] + [Trait("Issue", "1518")] + public void AddOcelotUsingBuilder_NoConfigurationParam_ShouldFindConfiguration() + { + // Arrange + var services = new ServiceCollection(); + var config = new ConfigurationBuilder().Build(); + services.AddSingleton(config); + + // Act + var ocelot = services.AddOcelotUsingBuilder(null, CustomBuilder); + + // Assert + AssertConfiguration(ocelot, config); + } + + [Theory] + [Trait("PR", "1986")] + [Trait("Issue", "1518")] + [InlineData(false)] + [InlineData(true)] + public void AddOcelotUsingBuilder_WithConfigurationParam_ShouldFindConfiguration(bool shouldFind) + { + // Arrange + var services = new ServiceCollection(); + var config = new ConfigurationBuilder().Build(); + if (shouldFind) + { + services.AddSingleton(config); + } + + // Act + var ocelot = services.AddOcelotUsingBuilder(shouldFind ? null : config, CustomBuilder); + + // Assert + AssertConfiguration(ocelot, config); + } + + private void AssertConfiguration(IOcelotBuilder ocelot, IConfiguration config) + { + ocelot.ShouldNotBeNull(); + var actual = ocelot.Configuration.ShouldNotBeNull(); + actual.Equals(config).ShouldBeTrue(); // check references equality + actual.ShouldBe(config); + Assert.Equal(1, _count); + } + + private int _count; + + private IMvcCoreBuilder CustomBuilder(IMvcCoreBuilder builder, Assembly assembly) + { + _count++; + return builder; + } +} diff --git a/test/Ocelot.UnitTests/DownstreamPathManipulation/ChangeDownstreamPathTemplateTests.cs b/test/Ocelot.UnitTests/DownstreamPathManipulation/ChangeDownstreamPathTemplateTests.cs index 3281aaa1ad..efe212166f 100644 --- a/test/Ocelot.UnitTests/DownstreamPathManipulation/ChangeDownstreamPathTemplateTests.cs +++ b/test/Ocelot.UnitTests/DownstreamPathManipulation/ChangeDownstreamPathTemplateTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.DownstreamPathManipulation { - public class ChangeDownstreamPathTemplateTests + public class ChangeDownstreamPathTemplateTests : UnitTest { private readonly ChangeDownstreamPathTemplate _changeDownstreamPath; private DownstreamPathTemplate _downstreamPathTemplate; diff --git a/test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs index 9be0a8f02b..4a1fcd7f3c 100644 --- a/test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs @@ -13,7 +13,7 @@ namespace Ocelot.UnitTests.DownstreamPathManipulation { - public class ClaimsToDownstreamPathMiddlewareTests + public class ClaimsToDownstreamPathMiddlewareTests : UnitTest { private readonly Mock _changePath; private readonly Mock _loggerFactory; diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs index 0ead908aa1..86ba4078a3 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder { - public class DownstreamRouteCreatorTests + public class DownstreamRouteCreatorTests : UnitTest { private readonly DownstreamRouteCreator _creator; private readonly QoSOptions _qoSOptions; diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs index f743f69141..6d331a36ee 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder { - public class DownstreamRouteFinderMiddlewareTests + public class DownstreamRouteFinderMiddlewareTests : UnitTest { private readonly Mock _finder; private readonly Mock _factory; diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs index 46654d8f4b..e9b2bf8ed0 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder { - public class DownstreamRouteFinderTests + public class DownstreamRouteFinderTests : UnitTest { private readonly IDownstreamRouteProvider _downstreamRouteFinder; private readonly Mock _mockMatcher; diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs index 209d67b723..db89a5eebe 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder { using Ocelot.DownstreamRouteFinder.Finder; - public class DownstreamRouteProviderFactoryTests + public class DownstreamRouteProviderFactoryTests : UnitTest { private readonly DownstreamRouteProviderFactory _factory; private IInternalConfiguration _config; diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs index 6cddb2f530..4117fc5627 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher { - public class RegExUrlMatcherTests + public class RegExUrlMatcherTests : UnitTest { private readonly IUrlPathToUrlTemplateMatcher _urlMatcher; private string _path; diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinderTests.cs index 606b637141..4dc610a508 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinderTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinderTests.cs @@ -3,7 +3,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher { - public class UrlPathPlaceholderNameAndValueFinderTests + public class UrlPathPlaceholderNameAndValueFinderTests : UnitTest { private readonly IPlaceholderNameAndValueFinder _finder; private string _downstreamUrlPath; diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamPathPlaceholderReplacerTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamPathPlaceholderReplacerTests.cs index b7ff203ddb..0f25294ee1 100644 --- a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamPathPlaceholderReplacerTests.cs +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamPathPlaceholderReplacerTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator { - public class DownstreamPathPlaceholderReplacerTests + public class DownstreamPathPlaceholderReplacerTests : UnitTest { private DownstreamRouteHolder _downstreamRoute; private Response _result; diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs index c6529e33de..615eb704ec 100644 --- a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs @@ -14,7 +14,7 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator { - public class DownstreamUrlCreatorMiddlewareTests + public class DownstreamUrlCreatorMiddlewareTests : UnitTest { private readonly Mock _downstreamUrlTemplateVariableReplacer; private OkResponse _downstreamPath; diff --git a/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs b/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs index b59c0bb684..ab72f90e26 100644 --- a/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.Errors { - public class ExceptionHandlerMiddlewareTests + public class ExceptionHandlerMiddlewareTests : UnitTest { private bool _shouldThrowAnException; private readonly Mock _repo; diff --git a/test/Ocelot.UnitTests/Eureka/EurekaServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Eureka/EurekaServiceDiscoveryProviderTests.cs index b14534afc7..fe92101c3d 100644 --- a/test/Ocelot.UnitTests/Eureka/EurekaServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/Eureka/EurekaServiceDiscoveryProviderTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.Eureka { - public class EurekaServiceDiscoveryProviderTests + public class EurekaServiceDiscoveryProviderTests : UnitTest { private readonly _Eureka_ _provider; private readonly Mock _client; diff --git a/test/Ocelot.UnitTests/FileUnitTest.cs b/test/Ocelot.UnitTests/FileUnitTest.cs new file mode 100644 index 0000000000..34c9de0a7f --- /dev/null +++ b/test/Ocelot.UnitTests/FileUnitTest.cs @@ -0,0 +1,102 @@ +using Ocelot.DependencyInjection; + +namespace Ocelot.UnitTests; + +public class FileUnitTest : UnitTest, IDisposable +{ + protected string _primaryConfigFileName; + protected string _globalConfigFileName; + protected string _environmentConfigFileName; + protected readonly List _files; + protected readonly List _folders; + + protected FileUnitTest() : this(null) { } + + protected FileUnitTest(string folder) + { + folder ??= TestID; + Directory.CreateDirectory(folder); + _folders = new() { folder }; + + _primaryConfigFileName = Path.Combine(folder, ConfigurationBuilderExtensions.PrimaryConfigFile); + _globalConfigFileName = Path.Combine(folder, ConfigurationBuilderExtensions.GlobalConfigFile); + _environmentConfigFileName = Path.Combine(folder, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, EnvironmentName())); + _files = new() + { + _primaryConfigFileName, + _globalConfigFileName, + _environmentConfigFileName, + }; + } + + protected virtual string EnvironmentName() => TestID; + + public virtual void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private bool _disposed; + + /// + /// Protected implementation of Dispose pattern. + /// + /// Flag to trigger actual disposing operation. + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + DeleteFiles(); + DeleteFolders(); + } + + _disposed = true; + } + + protected void DeleteFiles() + { + foreach (var file in _files) + { + try + { + var f = new FileInfo(file); + if (f.Exists) + { + f.Delete(); + } + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + } + + protected void DeleteFolders() + { + foreach (var folder in _folders) + { + try + { + var f = new DirectoryInfo(folder); + if (f.Exists && f.FullName != AppContext.BaseDirectory) + { + f.Delete(true); + } + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + } + + protected void TheOcelotPrimaryConfigFileExists(bool expected) + => File.Exists(_primaryConfigFileName).ShouldBe(expected); +} diff --git a/test/Ocelot.UnitTests/Headers/AddHeadersToRequestClaimToThingTests.cs b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestClaimToThingTests.cs index 8cfcca4fd4..d2a1cda2d2 100644 --- a/test/Ocelot.UnitTests/Headers/AddHeadersToRequestClaimToThingTests.cs +++ b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestClaimToThingTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Headers { - public class AddHeadersToRequestClaimToThingTests + public class AddHeadersToRequestClaimToThingTests : UnitTest { private readonly AddHeadersToRequest _addHeadersToRequest; private readonly Mock _parser; diff --git a/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs index d4c75ed7e4..0b30141cf4 100644 --- a/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs +++ b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Headers { - public class AddHeadersToRequestPlainTests + public class AddHeadersToRequestPlainTests : UnitTest { private readonly AddHeadersToRequest _addHeadersToRequest; private HttpContext _context; diff --git a/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs b/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs index 4563965bb7..b0a6e0514d 100644 --- a/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs +++ b/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.Headers { - public class AddHeadersToResponseTests + public class AddHeadersToResponseTests : UnitTest { private readonly IAddHeadersToResponse _adder; private readonly Mock _placeholders; diff --git a/test/Ocelot.UnitTests/Headers/ClaimsToHeadersMiddlewareTests.cs b/test/Ocelot.UnitTests/Headers/ClaimsToHeadersMiddlewareTests.cs index 7188500fdd..9e2eec2b17 100644 --- a/test/Ocelot.UnitTests/Headers/ClaimsToHeadersMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Headers/ClaimsToHeadersMiddlewareTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.Headers { - public class ClaimsToHeadersMiddlewareTests + public class ClaimsToHeadersMiddlewareTests : UnitTest { private readonly Mock _addHeaders; private Response _downstreamRoute; diff --git a/test/Ocelot.UnitTests/Headers/HttpContextRequestHeaderReplacerTests.cs b/test/Ocelot.UnitTests/Headers/HttpContextRequestHeaderReplacerTests.cs index 51fe342b1c..18021084d3 100644 --- a/test/Ocelot.UnitTests/Headers/HttpContextRequestHeaderReplacerTests.cs +++ b/test/Ocelot.UnitTests/Headers/HttpContextRequestHeaderReplacerTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.Headers { - public class HttpContextRequestHeaderReplacerTests + public class HttpContextRequestHeaderReplacerTests : UnitTest { private HttpContext _context; private List _fAndRs; diff --git a/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs b/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs index d9441462b0..f61c728960 100644 --- a/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Headers { - public class HttpHeadersTransformationMiddlewareTests + public class HttpHeadersTransformationMiddlewareTests : UnitTest { private readonly Mock _preReplacer; private readonly Mock _postReplacer; diff --git a/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs b/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs index 82e98c0f21..8ccef9447c 100644 --- a/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs +++ b/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Headers { - public class HttpResponseHeaderReplacerTests + public class HttpResponseHeaderReplacerTests : UnitTest { private DownstreamResponse _response; private readonly Placeholders _placeholders; diff --git a/test/Ocelot.UnitTests/Headers/RemoveHeadersTests.cs b/test/Ocelot.UnitTests/Headers/RemoveHeadersTests.cs index 12df115752..f391922953 100644 --- a/test/Ocelot.UnitTests/Headers/RemoveHeadersTests.cs +++ b/test/Ocelot.UnitTests/Headers/RemoveHeadersTests.cs @@ -3,7 +3,7 @@ namespace Ocelot.UnitTests.Headers { - public class RemoveHeadersTests + public class RemoveHeadersTests : UnitTest { private List
_headers; private readonly Ocelot.Headers.RemoveOutputHeaders _removeOutputHeaders; diff --git a/test/Ocelot.UnitTests/Infrastructure/ClaimParserTests.cs b/test/Ocelot.UnitTests/Infrastructure/ClaimParserTests.cs index ddcf726ace..9c5f7bf105 100644 --- a/test/Ocelot.UnitTests/Infrastructure/ClaimParserTests.cs +++ b/test/Ocelot.UnitTests/Infrastructure/ClaimParserTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.Infrastructure { - public class ClaimParserTests + public class ClaimParserTests : UnitTest { private readonly IClaimsParser _claimsParser; private readonly List _claims; diff --git a/test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs b/test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs index 191cc04993..17e21a6221 100644 --- a/test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Infrastructure { - public class HttpDataRepositoryTests + public class HttpDataRepositoryTests : UnitTest { private readonly HttpContext _httpContext; private readonly IHttpContextAccessor _httpContextAccessor; diff --git a/test/Ocelot.UnitTests/Infrastructure/IScopedRequestDataRepository.cs b/test/Ocelot.UnitTests/Infrastructure/IScopedRequestDataRepository.cs deleted file mode 100644 index 8b13789179..0000000000 --- a/test/Ocelot.UnitTests/Infrastructure/IScopedRequestDataRepository.cs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/Ocelot.UnitTests/Infrastructure/ScopesAuthorizerTests.cs b/test/Ocelot.UnitTests/Infrastructure/ScopesAuthorizerTests.cs index f580a746a2..b94774bbe2 100644 --- a/test/Ocelot.UnitTests/Infrastructure/ScopesAuthorizerTests.cs +++ b/test/Ocelot.UnitTests/Infrastructure/ScopesAuthorizerTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.Infrastructure { - public class ScopesAuthorizerTests + public class ScopesAuthorizerTests : UnitTest { private readonly ScopesAuthorizer _authorizer; public Mock _parser; diff --git a/test/Ocelot.UnitTests/Kubernetes/KubeServiceBuilderTests.cs b/test/Ocelot.UnitTests/Kubernetes/KubeServiceBuilderTests.cs new file mode 100644 index 0000000000..31f76c4e3f --- /dev/null +++ b/test/Ocelot.UnitTests/Kubernetes/KubeServiceBuilderTests.cs @@ -0,0 +1,162 @@ +using KubeClient.Models; +using Ocelot.Logging; +using Ocelot.Provider.Kubernetes; +using Ocelot.Provider.Kubernetes.Interfaces; +using Ocelot.Values; + +namespace Ocelot.UnitTests.Kubernetes; + +[Trait("Feat", "1967")] +public sealed class KubeServiceBuilderTests +{ + private readonly Mock factory; + private readonly Mock serviceCreator; + private readonly Mock logger; + private KubeServiceBuilder sut; + + public KubeServiceBuilderTests() + { + factory = new(); + serviceCreator = new(); + logger = new(); + } + + private void Arrange() + { + factory.Setup(x => x.CreateLogger()) + .Returns(logger.Object) + .Verifiable(); + logger.Setup(x => x.LogDebug(It.IsAny>())) + .Verifiable(); + sut = new KubeServiceBuilder(factory.Object, serviceCreator.Object); + } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + public void Cstor_NullArgs_ThrownException(bool isFactory, bool isServiceCreator) + { + // Arrange + var arg1 = isFactory ? factory.Object : null; + var arg2 = isServiceCreator ? serviceCreator.Object : null; + + // Act, Assert + Assert.Throws( + arg1 is null ? "factory" : arg2 is null ? "serviceCreator" : string.Empty, + () => sut = new KubeServiceBuilder(arg1, arg2)); + } + + [Fact] + public void Cstor_NotNullArgs_ObjCreated() + { + // Arrange + factory.Setup(x => x.CreateLogger()).Verifiable(); + + // Act + sut = new KubeServiceBuilder(factory.Object, serviceCreator.Object); + + // Assert + Assert.NotNull(sut); + factory.Verify(x => x.CreateLogger(), Times.Once()); + } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + public void BuildServices_NullArgs_ThrownException(bool isConfiguration, bool isEndpoint) + { + // Arrange + var arg1 = isConfiguration ? new KubeRegistryConfiguration() : null; + var arg2 = isEndpoint ? new EndpointsV1() : null; + Arrange(); + + // Act, Assert + Assert.Throws( + arg1 is null ? "configuration" : arg2 is null ? "endpoint" : string.Empty, + () => _ = sut.BuildServices(arg1, arg2)); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + public void BuildServices_WithSubsets_SelectedManyServicesPerSubset(int subsetCount) + { + // Arrange + var configuration = new KubeRegistryConfiguration(); + var endpoint = new EndpointsV1(); + for (int i = 1; i <= subsetCount; i++) + { + var subset = new EndpointSubsetV1(); + subset.Addresses.Add(new() { NodeName = "subset" + i, Hostname = i.ToString() }); + endpoint.Subsets.Add(subset); + } + + serviceCreator.Setup(x => x.Create(configuration, endpoint, It.IsAny())) + .Returns((c, e, s) => + { + var item = s.Addresses[0]; + int count = int.Parse(item.Hostname); + var list = new List(count); + while (count > 0) + { + var id = count--.ToString(); + list.Add(new Service($"{item.NodeName}-service{id}", null, id, id, null)); + } + + return list; + }); + var many = endpoint.Subsets.Sum(s => int.Parse(s.Addresses[0].Hostname)); + Arrange(); + + // Act + var actual = sut.BuildServices(configuration, endpoint); + + // Assert + Assert.NotNull(actual); + var l = actual.ToList(); + Assert.Equal(many, l.Count); + serviceCreator.Verify(x => x.Create(configuration, endpoint, It.IsAny()), + Times.Exactly(endpoint.Subsets.Count)); + logger.Verify(x => x.LogDebug(It.IsAny>()), + Times.Once()); + } + + [Theory] + [InlineData(false, false, false, false, "K8s '?:?:?' endpoint: Total built 0 services.")] + [InlineData(false, false, false, true, "K8s '?:?:?' endpoint: Total built 0 services.")] + [InlineData(false, false, true, false, "K8s '?:?:?' endpoint: Total built 0 services.")] + [InlineData(false, false, true, true, "K8s '?:?:Name' endpoint: Total built 0 services.")] + [InlineData(false, true, true, true, "K8s '?:ApiVersion:Name' endpoint: Total built 0 services.")] + [InlineData(true, true, true, true, "K8s 'Kind:ApiVersion:Name' endpoint: Total built 0 services.")] + public void BuildServices_WithEndpoint_LogDebug(bool hasKind, bool hasApiVersion, bool hasMetadata, bool hasMetadataName, string message) + { + // Arrange + var configuration = new KubeRegistryConfiguration(); + var endpoint = new EndpointsV1() + { + Kind = hasKind ? nameof(EndpointsV1.Kind) : null, + ApiVersion = hasApiVersion ? nameof(EndpointsV1.ApiVersion) : null, + Metadata = hasMetadata ? new() + { + Name = hasMetadataName ? nameof(ObjectMetaV1.Name) : null, + } : null, + }; + Arrange(); + string actualMesssage = null; + logger.Setup(x => x.LogDebug(It.IsAny>())) + .Callback>(f => actualMesssage = f.Invoke()); + + // Act + var actual = sut.BuildServices(configuration, endpoint); + + // Assert + Assert.NotNull(actual); + logger.Verify(x => x.LogDebug(It.IsAny>()), + Times.Once()); + Assert.NotNull(actualMesssage); + Assert.Equal(message, actualMesssage); + } +} diff --git a/test/Ocelot.UnitTests/Kubernetes/KubeServiceCreatorTests.cs b/test/Ocelot.UnitTests/Kubernetes/KubeServiceCreatorTests.cs new file mode 100644 index 0000000000..a8794ff877 --- /dev/null +++ b/test/Ocelot.UnitTests/Kubernetes/KubeServiceCreatorTests.cs @@ -0,0 +1,150 @@ +using KubeClient.Models; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Ocelot.Logging; +using Ocelot.Provider.Kubernetes; + +namespace Ocelot.UnitTests.Kubernetes; + +[Trait("Feat", "1967")] +public sealed class KubeServiceCreatorTests +{ + private readonly Mock factory; + private readonly Mock logger; + private KubeServiceCreator sut; + + public KubeServiceCreatorTests() + { + factory = new(); + logger = new(); + } + + private void Arrange() + { + factory.Setup(x => x.CreateLogger()) + .Returns(logger.Object) + .Verifiable(); + logger.Setup(x => x.LogDebug(It.IsAny>())) + .Verifiable(); + sut = new KubeServiceCreator(factory.Object); + } + + [Fact] + public void Cstor_NullArg_ThrownException() + { + // Arrange, Act, Assert + Assert.Throws("factory", + () => sut = new KubeServiceCreator(null)); + } + + [Fact] + public void Cstor_NotNullArg_ObjCreated() + { + // Arrange + factory.Setup(x => x.CreateLogger()).Verifiable(); + + // Act + sut = new KubeServiceCreator(factory.Object); + + // Assert + Assert.NotNull(sut); + factory.Verify(x => x.CreateLogger(), Times.Once()); + } + + [Theory] + [InlineData(false, true, true)] + [InlineData(true, false, true)] + [InlineData(true, true, false)] + public void Create_NullArgs_ReturnedEmpty(bool isConfiguration, bool isEndpoint, bool isSubset) + { + // Arrange + var arg1 = isConfiguration ? new KubeRegistryConfiguration() : null; + var arg2 = isEndpoint ? new EndpointsV1() : null; + var arg3 = isSubset ? new EndpointSubsetV1() : null; + Arrange(); + + // Act + var actual = sut.Create(arg1, arg2, arg3); + + // Assert + Assert.NotNull(actual); + Assert.Empty(actual); + } + + [Fact(DisplayName = "Create: With empty args -> No exceptions during creation")] + public void Create_NotNullButEmptyArgs_CreatedEmptyService() + { + // Arrange + var arg1 = new KubeRegistryConfiguration() + { + KubeNamespace = nameof(KubeServiceCreatorTests), + KeyOfServiceInK8s = nameof(Create_NotNullButEmptyArgs_CreatedEmptyService), + }; + var arg2 = new EndpointsV1(); + var arg3 = new EndpointSubsetV1(); + arg3.Addresses.Add(new()); + arg2.Subsets.Add(arg3); + Arrange(); + + // Act + var actual = sut.Create(arg1, arg2, arg3); + + // Assert + Assert.NotNull(actual); + Assert.NotEmpty(actual); + var actualService = actual.SingleOrDefault(); + Assert.NotNull(actualService); + Assert.Null(actualService.Name); + } + + [Fact] + public void Create_ValidArgs_HappyPath() + { + // Arrange + var arg1 = new KubeRegistryConfiguration() + { + KubeNamespace = nameof(KubeServiceCreatorTests), + KeyOfServiceInK8s = nameof(Create_ValidArgs_HappyPath), + Scheme = "happy", //nameof(HttpScheme.Http), + }; + var arg2 = new EndpointsV1() + { + ApiVersion = "v1", + Metadata = new() + { + Namespace = nameof(KubeServiceCreatorTests), + Name = nameof(Create_ValidArgs_HappyPath), + Uid = Guid.NewGuid().ToString(), + }, + }; + var arg3 = new EndpointSubsetV1(); + arg3.Addresses.Add(new() + { + Ip = "8.8.8.8", + NodeName = "google", + Hostname = "dns.google", + }); + var ports = new List + { + new() { Name = nameof(HttpScheme.Http), Port = 80 }, + new() { Name = "happy", Port = 888 }, + }; + arg3.Ports.AddRange(ports); + arg2.Subsets.Add(arg3); + Arrange(); + + // Act + var actual = sut.Create(arg1, arg2, arg3); + + // Assert + Assert.NotNull(actual); + Assert.NotEmpty(actual); + var service = actual.SingleOrDefault(); + Assert.NotNull(service); + Assert.Equal(nameof(Create_ValidArgs_HappyPath), service.Name); + Assert.Equal("happy", service.HostAndPort.Scheme); + Assert.Equal(888, service.HostAndPort.DownstreamPort); + Assert.Equal("8.8.8.8", service.HostAndPort.DownstreamHost); + logger.Verify(x => x.LogDebug(It.IsAny>()), + Times.Once()); + } +} diff --git a/test/Ocelot.UnitTests/Kubernetes/KubeTests.cs b/test/Ocelot.UnitTests/Kubernetes/KubeTests.cs index 49e91fe6ea..213a25f65f 100644 --- a/test/Ocelot.UnitTests/Kubernetes/KubeTests.cs +++ b/test/Ocelot.UnitTests/Kubernetes/KubeTests.cs @@ -6,6 +6,7 @@ using Newtonsoft.Json; using Ocelot.Logging; using Ocelot.Provider.Kubernetes; +using Ocelot.Provider.Kubernetes.Interfaces; using Ocelot.Values; namespace Ocelot.UnitTests.Kubernetes @@ -21,10 +22,11 @@ public class KubeTests : IDisposable private readonly string _kubeHost; private readonly string _fakekubeServiceDiscoveryUrl; private List _services; + private string _receivedToken; private readonly Mock _factory; private readonly Mock _logger; - private string _receivedToken; private readonly IKubeApiClient _clientFactory; + private readonly Mock _serviceBuilder; public KubeTests() { @@ -33,8 +35,8 @@ public KubeTests() _port = 5567; _kubeHost = "localhost"; _fakekubeServiceDiscoveryUrl = $"{Uri.UriSchemeHttp}://{_kubeHost}:{_port}"; - _endpointEntries = new EndpointsV1(); - _factory = new Mock(); + _endpointEntries = new(); + _factory = new(); var option = new KubeClientOptions { @@ -45,19 +47,21 @@ public KubeTests() }; _clientFactory = KubeApiClient.Create(option); - _logger = new Mock(); + _logger = new(); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); var config = new KubeRegistryConfiguration { KeyOfServiceInK8s = _serviceName, KubeNamespace = _namespaces, }; - _provider = new Kube(config, _factory.Object, _clientFactory); + _serviceBuilder = new(); + _provider = new Kube(config, _factory.Object, _clientFactory, _serviceBuilder.Object); } [Fact] public void Should_return_service_from_k8s() { + // Arrange var token = "Bearer txpc696iUhbVoudg164r93CxDTrKRVWG"; var endPointEntryOne = new EndpointsV1 { @@ -65,6 +69,7 @@ public void Should_return_service_from_k8s() ApiVersion = "1.0", Metadata = new ObjectMetaV1 { + Name = nameof(Should_return_service_from_k8s), Namespace = "dev", }, }; @@ -79,13 +84,17 @@ public void Should_return_service_from_k8s() Port = 80, }); endPointEntryOne.Subsets.Add(endpointSubsetV1); + _serviceBuilder.Setup(x => x.BuildServices(It.IsAny(), It.IsAny())) + .Returns(new Service[] { new(nameof(Should_return_service_from_k8s), new("localhost", 80), string.Empty, string.Empty, new string[0]) }); + GivenThereIsAFakeKubeServiceDiscoveryProvider(_fakekubeServiceDiscoveryUrl, _serviceName, _namespaces); + GivenTheServicesAreRegisteredWithKube(endPointEntryOne); + + // Act + WhenIGetTheServices(); - this.Given(x => GivenThereIsAFakeKubeServiceDiscoveryProvider(_fakekubeServiceDiscoveryUrl, _serviceName, _namespaces)) - .And(x => GivenTheServicesAreRegisteredWithKube(endPointEntryOne)) - .When(x => WhenIGetTheServices()) - .Then(x => ThenTheCountIs(1)) - .And(_ => ThenTheTokenIs(token)) - .BDDfy(); + // Assert + ThenTheCountIs(1); + ThenTheTokenIs(token); } private void ThenTheTokenIs(string token) diff --git a/test/Ocelot.UnitTests/Kubernetes/OcelotBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/Kubernetes/OcelotBuilderExtensionsTests.cs index 2f32b562d7..d3c0b9967e 100644 --- a/test/Ocelot.UnitTests/Kubernetes/OcelotBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Kubernetes/OcelotBuilderExtensionsTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.Kubernetes { - public class OcelotBuilderExtensionsTests + public class OcelotBuilderExtensionsTests : UnitTest { private readonly IServiceCollection _services; private readonly IConfiguration _configRoot; diff --git a/test/Ocelot.UnitTests/Kubernetes/PollKubeTests.cs b/test/Ocelot.UnitTests/Kubernetes/PollKubeTests.cs index d041288f65..794589bc33 100644 --- a/test/Ocelot.UnitTests/Kubernetes/PollKubeTests.cs +++ b/test/Ocelot.UnitTests/Kubernetes/PollKubeTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.Kubernetes { - public class PollKubeTests + public class PollKubeTests : UnitTest { private readonly int _delay; private PollKube _provider; diff --git a/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsCreatorTests.cs index 601e837e99..682ea9d416 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsCreatorTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class CookieStickySessionsCreatorTests + public class CookieStickySessionsCreatorTests : UnitTest { private readonly CookieStickySessionsCreator _creator; private readonly Mock _serviceProvider; diff --git a/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs b/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs index 615218deb2..71c4d05177 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class CookieStickySessionsTests + public class CookieStickySessionsTests : UnitTest { private readonly CookieStickySessions _stickySessions; private readonly Mock _loadBalancer; diff --git a/test/Ocelot.UnitTests/LoadBalancer/DelegateInvokingLoadBalancerCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/DelegateInvokingLoadBalancerCreatorTests.cs index 6631051fc0..deb25a12ed 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/DelegateInvokingLoadBalancerCreatorTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/DelegateInvokingLoadBalancerCreatorTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class DelegateInvokingLoadBalancerCreatorTests + public class DelegateInvokingLoadBalancerCreatorTests : UnitTest { private DelegateInvokingLoadBalancerCreator _creator; private Func _creatorFunc; diff --git a/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionCreatorTests.cs index e94cfc5b62..e56fa8f7b8 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionCreatorTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionCreatorTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class LeastConnectionCreatorTests + public class LeastConnectionCreatorTests : UnitTest { private readonly LeastConnectionCreator _creator; private readonly Mock _serviceProvider; diff --git a/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs index 2766f58ab1..368e866e6d 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class LeastConnectionTests + public class LeastConnectionTests : UnitTest { private ServiceHostAndPort _hostAndPort; private Response _result; diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs index 5eb21c6d77..616168548f 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class LoadBalancerFactoryTests + public class LoadBalancerFactoryTests : UnitTest { private DownstreamRoute _route; private readonly LoadBalancerFactory _factory; diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs index 216bda84ee..fa0b835ff9 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class LoadBalancerHouseTests + public class LoadBalancerHouseTests : UnitTest { private DownstreamRoute _route; private ILoadBalancer _loadBalancer; diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs index 411c43fd47..f5b5dd925c 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs @@ -13,7 +13,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class LoadBalancerMiddlewareTests + public class LoadBalancerMiddlewareTests : UnitTest { private readonly Mock _loadBalancerHouse; private readonly Mock _loadBalancer; diff --git a/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerCreatorTests.cs index df478ec241..dadf453e1e 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerCreatorTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerCreatorTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class NoLoadBalancerCreatorTests + public class NoLoadBalancerCreatorTests : UnitTest { private readonly NoLoadBalancerCreator _creator; private readonly Mock _serviceProvider; diff --git a/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs index 5e562352a0..e1490e898c 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class NoLoadBalancerTests + public class NoLoadBalancerTests : UnitTest { private readonly List _services; private NoLoadBalancer _loadBalancer; diff --git a/test/Ocelot.UnitTests/LoadBalancer/RoundRobinCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinCreatorTests.cs index 2b3d00d8aa..13f5b6622a 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/RoundRobinCreatorTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinCreatorTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class RoundRobinCreatorTests + public class RoundRobinCreatorTests : UnitTest { private readonly RoundRobinCreator _creator; private readonly Mock _serviceProvider; diff --git a/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs index 78196c13eb..af55d65aa4 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class RoundRobinTests + public class RoundRobinTests : UnitTest { private readonly RoundRobin _roundRobin; private readonly List _services; diff --git a/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs b/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs index d7ac54b64d..94d3da1c70 100644 --- a/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs +++ b/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Logging { - public class OcelotDiagnosticListenerTests + public class OcelotDiagnosticListenerTests : UnitTest { private readonly OcelotDiagnosticListener _listener; private readonly Mock _factory; diff --git a/test/Ocelot.UnitTests/Middleware/BaseUrlFinderTests.cs b/test/Ocelot.UnitTests/Middleware/BaseUrlFinderTests.cs index 4049e08aaa..e6db74c33e 100644 --- a/test/Ocelot.UnitTests/Middleware/BaseUrlFinderTests.cs +++ b/test/Ocelot.UnitTests/Middleware/BaseUrlFinderTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Middleware { - public class BaseUrlFinderTests + public class BaseUrlFinderTests : UnitTest { private BaseUrlFinder _baseUrlFinder; private IConfiguration _config; diff --git a/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs b/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs index f7f41ae6a6..7d2e4b75ad 100644 --- a/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs @@ -12,7 +12,7 @@ namespace Ocelot.UnitTests.Middleware { - public class OcelotPipelineExtensionsTests + public class OcelotPipelineExtensionsTests : UnitTest { private ApplicationBuilder _builder; private RequestDelegate _handlers; diff --git a/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs b/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs index 0cd2147c36..2948b70a68 100644 --- a/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs +++ b/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.Middleware { - public class OcelotPiplineBuilderTests + public class OcelotPiplineBuilderTests : UnitTest { private readonly IServiceCollection _services; private readonly IConfiguration _configRoot; diff --git a/test/Ocelot.UnitTests/Multiplexing/DefinedAggregatorProviderTests.cs b/test/Ocelot.UnitTests/Multiplexing/DefinedAggregatorProviderTests.cs index b9a6c91b31..7a1c8e9144 100644 --- a/test/Ocelot.UnitTests/Multiplexing/DefinedAggregatorProviderTests.cs +++ b/test/Ocelot.UnitTests/Multiplexing/DefinedAggregatorProviderTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.Multiplexing { - public class DefinedAggregatorProviderTests + public class DefinedAggregatorProviderTests : UnitTest { private ServiceLocatorDefinedAggregatorProvider _provider; private Response _aggregator; diff --git a/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs b/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs index a7e6d2cb2b..b94c247ff8 100644 --- a/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs @@ -1,38 +1,47 @@ using Microsoft.AspNetCore.Http; +using Moq.Protected; using Ocelot.Configuration; using Ocelot.Configuration.Builder; +using Ocelot.Configuration.File; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Multiplexer; +using System.Reflection; +using System.Security.Claims; +using System.Text; namespace Ocelot.UnitTests.Multiplexing { - public class MultiplexingMiddlewareTests + public class MultiplexingMiddlewareTests : UnitTest { - private readonly MultiplexingMiddleware _middleware; + private MultiplexingMiddleware _middleware; private Ocelot.DownstreamRouteFinder.DownstreamRouteHolder _downstreamRoute; private int _count; private readonly HttpContext _httpContext; + private readonly Mock factory; + private readonly Mock aggregator; + private readonly Mock loggerFactory; + private readonly Mock logger; public MultiplexingMiddlewareTests() { _httpContext = new DefaultHttpContext(); - var factory = new Mock(); - var aggregator = new Mock(); + factory = new Mock(); + aggregator = new Mock(); factory.Setup(x => x.Get(It.IsAny())).Returns(aggregator.Object); - var loggerFactory = new Mock(); - var logger = new Mock(); + loggerFactory = new Mock(); + logger = new Mock(); loggerFactory.Setup(x => x.CreateLogger()).Returns(logger.Object); - Task Next(HttpContext context) => Task.FromResult(_count++); _middleware = new MultiplexingMiddleware(Next, loggerFactory.Object, factory.Object); } + private Task Next(HttpContext context) => Task.FromResult(_count++); + [Fact] public void should_multiplex() { - var route = new RouteBuilder().WithDownstreamRoute(new DownstreamRouteBuilder().Build()).WithDownstreamRoute(new DownstreamRouteBuilder().Build()).Build(); - + var route = GivenDefaultRoute(2); this.Given(x => GivenTheFollowing(route)) .When(x => WhenIMultiplex()) .Then(x => ThePipelineIsCalled(2)) @@ -50,15 +59,308 @@ public void should_not_multiplex() .BDDfy(); } + [Fact] + [Trait("Bug", "1396")] + public async Task CreateThreadContextAsync_CopyUser_ToTarget() + { + // Arrange + GivenUser("test", "Copy", nameof(CreateThreadContextAsync_CopyUser_ToTarget)); + + // Act + var method = _middleware.GetType().GetMethod("CreateThreadContextAsync", BindingFlags.NonPublic | BindingFlags.Instance); + var actual = await (Task)method.Invoke(_middleware, new object[] { _httpContext }); + + // Assert + AssertUsers(actual); + } + + [Fact] + [Trait("Bug", "1396")] + public async Task Invoke_ContextUser_ForwardedToDownstreamContext() + { + // Setup + HttpContext actualContext = null; + _middleware = new MultiplexingMiddleware(NextMe, loggerFactory.Object, factory.Object); + Task NextMe(HttpContext context) + { + actualContext = context; + return Next(context); + } + + // Arrange + GivenUser("test", "Invoke", nameof(Invoke_ContextUser_ForwardedToDownstreamContext)); + GivenTheFollowing(GivenDefaultRoute(2)); + + // Act + await WhenIMultiplex(); + + // Assert + ThePipelineIsCalled(2); + AssertUsers(actualContext); + } + + [Fact] + [Trait("PR", "1826")] + public async Task Should_Not_Copy_Context_If_One_Downstream_Route() + { + _middleware = new MultiplexingMiddleware(NextMe, loggerFactory.Object, factory.Object); + Task NextMe(HttpContext context) + { + Assert.Equal(_httpContext, context); + return Next(context); + } + + // Arrange + GivenUser("test", "Invoke", nameof(Should_Not_Copy_Context_If_One_Downstream_Route)); + GivenTheFollowing(GivenDefaultRoute(1)); + + // Act + await WhenIMultiplex(); + + // Assert + ThePipelineIsCalled(1); + } + + [Fact] + [Trait("PR", "1826")] + public async Task Should_Call_ProcessSingleRoute_Once_If_One_Downstream_Route() + { + var mock = MockMiddlewareFactory(null, null); + + _middleware = mock.Object; + + // Arrange + GivenUser("test", "Invoke", nameof(Should_Call_ProcessSingleRoute_Once_If_One_Downstream_Route)); + GivenTheFollowing(GivenDefaultRoute(1)); + + // Act + await WhenIMultiplex(); + + // Assert + mock.Protected().Verify("ProcessSingleRouteAsync", Times.Once(), + ItExpr.IsAny(), + ItExpr.IsAny()); + } + + [Theory] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + [Trait("PR", "1826")] + public async Task Should_Not_Call_ProcessSingleRoute_If_More_Than_One_Downstream_Route(int routesCount) + { + var mock = MockMiddlewareFactory(null, null); + + // Arrange + GivenUser("test", "Invoke", nameof(Should_Not_Call_ProcessSingleRoute_If_More_Than_One_Downstream_Route)); + GivenTheFollowing(GivenDefaultRoute(routesCount)); + + // Act + await WhenIMultiplex(); + + // Assert + mock.Protected().Verify("ProcessSingleRouteAsync", Times.Never(), + ItExpr.IsAny(), + ItExpr.IsAny()); + } + + [Theory] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + [Trait("PR", "1826")] + public async Task Should_Create_As_Many_Contexts_As_Routes_And_Map_Is_Called_Once(int routesCount) + { + var mock = MockMiddlewareFactory(routesCount, null); + + // Arrange + GivenUser("test", "Invoke", nameof(Should_Create_As_Many_Contexts_As_Routes_And_Map_Is_Called_Once)); + GivenTheFollowing(GivenDefaultRoute(routesCount)); + + // Act + await WhenIMultiplex(); + + // Assert + mock.Protected().Verify("MapAsync", Times.Once(), + ItExpr.IsAny(), + ItExpr.IsAny(), + ItExpr.Is>(list => list.Count == routesCount) + ); + } + + [Fact] + [Trait("PR", "1826")] + public async Task Should_Not_Call_ProcessSingleRoute_Or_Map_If_No_Route() + { + var mock = MockMiddlewareFactory(null, null); + + // Arrange + GivenUser("test", "Invoke", nameof(Should_Not_Call_ProcessSingleRoute_Or_Map_If_No_Route)); + GivenTheFollowing(GivenDefaultRoute(0)); + + // Act + await WhenIMultiplex(); + + // Assert + mock.Protected().Verify("ProcessSingleRouteAsync", Times.Never(), + ItExpr.IsAny(), + ItExpr.IsAny()); + + mock.Protected().Verify("MapAsync", Times.Never(), + ItExpr.IsAny(), + ItExpr.IsAny(), + ItExpr.IsAny>()); + } + + [Theory] + [Trait("Bug", "2039")] + [InlineData(1)] // Times.Never() + [InlineData(2)] // Times.Exactly(2) + [InlineData(3)] // Times.Exactly(3) + [InlineData(4)] // Times.Exactly(4) + public async Task Should_Call_CloneRequestBodyAsync_Each_Time_Per_Requests(int numberOfRoutes) + { + // Arrange + var mock = MockMiddlewareFactory(null, null); + GivenUser("test", "Invoke", nameof(Should_Call_CloneRequestBodyAsync_Each_Time_Per_Requests)); + GivenTheFollowing(GivenDefaultRoute(numberOfRoutes)); + + // Act + await WhenIMultiplex(); + + // Assert + mock.Protected().Verify>("CloneRequestBodyAsync", + numberOfRoutes > 1 ? Times.Exactly(numberOfRoutes) : Times.Never(), + ItExpr.IsAny(), + ItExpr.IsAny()); + } + + [Fact] + [Trait("PR", "1826")] + public async Task If_Using_3_Routes_WithAggregator_ProcessSingleRoute_Is_Never_Called_Map_Once_And_Pipeline_3_Times() + { + var mock = MockMiddlewareFactory(null, AggregateRequestDelegateFactory()); + + // Arrange + GivenUser("test", "Invoke", nameof(If_Using_3_Routes_WithAggregator_ProcessSingleRoute_Is_Never_Called_Map_Once_And_Pipeline_3_Times)); + GivenTheFollowing(GivenRoutesWithAggregator()); + + // Act + await WhenIMultiplex(); + + mock.Protected().Verify("ProcessSingleRouteAsync", Times.Never(), + ItExpr.IsAny(), + ItExpr.IsAny()); + + mock.Protected().Verify("MapAsync", Times.Once(), + ItExpr.IsAny(), + ItExpr.IsAny(), + ItExpr.IsAny>()); + + ThePipelineIsCalled(3); + } + + private RequestDelegate AggregateRequestDelegateFactory() + { + return context => + { + var responseContent = @"[{""id"":1,""writerId"":1,""postId"":2,""text"":""text1""},{""id"":2,""writerId"":1,""postId"":2,""text"":""text2""}]"; + context.Items.Add("DownstreamResponse", new DownstreamResponse(new StringContent(responseContent, Encoding.UTF8, "application/json"), HttpStatusCode.OK, new List
(), "test")); + + if (!context.Items.ContainsKey("TemplatePlaceholderNameAndValues")) + { + context.Items.Add("TemplatePlaceholderNameAndValues", new List()); + } + + _count++; + return Task.CompletedTask; + }; + } + + private Mock MockMiddlewareFactory(int? downstreamRoutesCount, RequestDelegate requestDelegate) + { + requestDelegate ??= Next; + + var mock = new Mock(requestDelegate, loggerFactory.Object, factory.Object) { CallBase = true }; + + mock.Protected().Setup("MapAsync", + ItExpr.IsAny(), + ItExpr.IsAny(), + downstreamRoutesCount == null ? ItExpr.IsAny>() : ItExpr.Is>(list => list.Count == downstreamRoutesCount) + ).Returns(Task.CompletedTask).Verifiable(); + + mock.Protected().Setup("ProcessSingleRouteAsync", + ItExpr.IsAny(), + ItExpr.IsAny() + ).Returns(Task.CompletedTask).Verifiable(); + + _middleware = mock.Object; + return mock; + } + + private void GivenUser(string authentication, string name, string role) + { + var user = new ClaimsPrincipal(); + user.AddIdentity(new(authentication, name, role)); + _httpContext.User = user; + } + + private void AssertUsers(HttpContext actual) + { + Assert.NotNull(actual); + Assert.Same(_httpContext.User, actual.User); + Assert.NotNull(actual.User.Identity); + var identity = _httpContext.User.Identity as ClaimsIdentity; + var actualIdentity = actual.User.Identity as ClaimsIdentity; + Assert.Equal(identity.AuthenticationType, actualIdentity.AuthenticationType); + Assert.Equal(identity.NameClaimType, actualIdentity.NameClaimType); + Assert.Equal(identity.RoleClaimType, actualIdentity.RoleClaimType); + } + + private static Route GivenDefaultRoute(int count) + { + var b = new RouteBuilder(); + for (var i = 0; i < count; i++) + { + b.WithDownstreamRoute(new DownstreamRouteBuilder().Build()); + } + + return b.Build(); + } + + private static Route GivenRoutesWithAggregator() + { + var route1 = new DownstreamRouteBuilder().WithKey("Comments").Build(); + var route2 = new DownstreamRouteBuilder().WithKey("UserDetails").Build(); + var route3 = new DownstreamRouteBuilder().WithKey("PostDetails").Build(); + + var b = new RouteBuilder(); + b.WithDownstreamRoute(route1); + b.WithDownstreamRoute(route2); + b.WithDownstreamRoute(route3); + + b.WithAggregateRouteConfig(new() + { + new AggregateRouteConfig { RouteKey = "UserDetails", JsonPath = "$[*].writerId", Parameter = "userId" }, + new AggregateRouteConfig { RouteKey = "PostDetails", JsonPath = "$[*].postId", Parameter = "postId" }, + }); + + b.WithAggregator("TestAggregator"); + + return b.Build(); + } + private void GivenTheFollowing(Route route) { _downstreamRoute = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(new List(), route); _httpContext.Items.UpsertDownstreamRoute(_downstreamRoute); } - private void WhenIMultiplex() + private async Task WhenIMultiplex() { - _middleware.Invoke(_httpContext).GetAwaiter().GetResult(); + await _middleware.Invoke(_httpContext); } private void ThePipelineIsCalled(int expected) diff --git a/test/Ocelot.UnitTests/Multiplexing/ResponseAggregatorFactoryTests.cs b/test/Ocelot.UnitTests/Multiplexing/ResponseAggregatorFactoryTests.cs index 7db1d8faf5..15a229573d 100644 --- a/test/Ocelot.UnitTests/Multiplexing/ResponseAggregatorFactoryTests.cs +++ b/test/Ocelot.UnitTests/Multiplexing/ResponseAggregatorFactoryTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Multiplexing { - public class ResponseAggregatorFactoryTests + public class ResponseAggregatorFactoryTests : UnitTest { private readonly InMemoryResponseAggregatorFactory _factory; private readonly Mock _provider; diff --git a/test/Ocelot.UnitTests/Multiplexing/SimpleJsonResponseAggregatorTests.cs b/test/Ocelot.UnitTests/Multiplexing/SimpleJsonResponseAggregatorTests.cs index 45f7f2d920..fafda9bfdd 100644 --- a/test/Ocelot.UnitTests/Multiplexing/SimpleJsonResponseAggregatorTests.cs +++ b/test/Ocelot.UnitTests/Multiplexing/SimpleJsonResponseAggregatorTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.Multiplexing { - public class SimpleJsonResponseAggregatorTests + public class SimpleJsonResponseAggregatorTests : UnitTest { private readonly SimpleJsonResponseAggregator _aggregator; private List _downstreamContexts; diff --git a/test/Ocelot.UnitTests/Multiplexing/UserDefinedResponseAggregatorTests.cs b/test/Ocelot.UnitTests/Multiplexing/UserDefinedResponseAggregatorTests.cs index a960dc119c..4a0349795a 100644 --- a/test/Ocelot.UnitTests/Multiplexing/UserDefinedResponseAggregatorTests.cs +++ b/test/Ocelot.UnitTests/Multiplexing/UserDefinedResponseAggregatorTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.Multiplexing { - public class UserDefinedResponseAggregatorTests + public class UserDefinedResponseAggregatorTests : UnitTest { private readonly UserDefinedResponseAggregator _aggregator; private readonly Mock _provider; diff --git a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj index cb25575623..b9f107cfcc 100644 --- a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj +++ b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj @@ -16,7 +16,8 @@ false ..\..\codeanalysis.ruleset True - 1591 + ..\..\codeanalysis.ruleset + $(NoWarn);CS0618;CS1591 full @@ -26,12 +27,12 @@ - + @@ -50,6 +51,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -73,7 +75,7 @@ - + diff --git a/test/Ocelot.UnitTests/Polly/OcelotBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/Polly/OcelotBuilderExtensionsTests.cs index 81147b56a3..5dffec17ff 100644 --- a/test/Ocelot.UnitTests/Polly/OcelotBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Polly/OcelotBuilderExtensionsTests.cs @@ -19,7 +19,7 @@ public void Should_build() var services = new ServiceCollection(); var options = new QoSOptionsBuilder() .WithTimeoutValue(100) - .WithExceptionsAllowedBeforeBreaking(1) + .WithExceptionsAllowedBeforeBreaking(2) .WithDurationOfBreak(200) .Build(); var route = new DownstreamRouteBuilder().WithQosOptions(options) diff --git a/test/Ocelot.UnitTests/Polly/PollyPoliciesDelegatingHandlerTests.cs b/test/Ocelot.UnitTests/Polly/PollyPoliciesDelegatingHandlerTests.cs index 872fa635a7..f968fb5ee6 100644 --- a/test/Ocelot.UnitTests/Polly/PollyPoliciesDelegatingHandlerTests.cs +++ b/test/Ocelot.UnitTests/Polly/PollyPoliciesDelegatingHandlerTests.cs @@ -3,10 +3,10 @@ using Ocelot.Configuration.Builder; using Ocelot.Logging; using Ocelot.Provider.Polly; -using Ocelot.Provider.Polly.Interfaces; using Polly; using Polly.Wrap; using System.Reflection; +using Ocelot.Provider.Polly.v7; namespace Ocelot.UnitTests.Polly; diff --git a/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs b/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs index 6ebfeee938..8c08a46e06 100644 --- a/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs +++ b/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs @@ -1,6 +1,7 @@ using Ocelot.Configuration.Builder; using Ocelot.Logging; using Ocelot.Provider.Polly; +using Ocelot.Provider.Polly.v7; using Polly; using Polly.CircuitBreaker; using Polly.Timeout; diff --git a/test/Ocelot.UnitTests/Polly/PollyQoSResiliencePipelineProviderTests.cs b/test/Ocelot.UnitTests/Polly/PollyQoSResiliencePipelineProviderTests.cs new file mode 100644 index 0000000000..1099a22811 --- /dev/null +++ b/test/Ocelot.UnitTests/Polly/PollyQoSResiliencePipelineProviderTests.cs @@ -0,0 +1,251 @@ +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Logging; +using Ocelot.Provider.Polly; +using Polly.CircuitBreaker; +using Polly.Registry; +using Polly.Testing; +using Polly.Timeout; + +namespace Ocelot.UnitTests.Polly; + +public class PollyQoSResiliencePipelineProviderTests +{ + [Fact] + public void Should_build() + { + var options = new QoSOptionsBuilder() + .WithTimeoutValue(1000) // 1s, minimum required by Polly + .WithExceptionsAllowedBeforeBreaking(2) // 2 is the minimum required by Polly + .WithDurationOfBreak(500) // 0.5s, minimum required by Polly + .Build(); + + var route = new DownstreamRouteBuilder() + .WithQosOptions(options) + .Build(); + + var loggerFactoryMock = new Mock(); + var resiliencePipelineRegistry = new ResiliencePipelineRegistry(); + var pollyQoSResiliencePipelineProvider = new PollyQoSResiliencePipelineProvider(loggerFactoryMock.Object, resiliencePipelineRegistry); + var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); + resiliencePipeline.ShouldNotBeNull(); + } + + [Fact] + public void Should_return_same_circuit_breaker_for_given_route() + { + var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); + var route1 = DownstreamRouteFactory("/"); + var route2 = DownstreamRouteFactory("/"); + + var resiliencePipeline1 = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route1); + var resiliencePipeline2 = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route2); + resiliencePipeline1.ShouldBe(resiliencePipeline2); + + var resiliencePipeline3 = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route1); + resiliencePipeline3.ShouldBe(resiliencePipeline1); + resiliencePipeline3.ShouldBe(resiliencePipeline2); + } + + [Fact] + public void Should_return_different_circuit_breaker_for_two_different_routes() + { + var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); + var route1 = DownstreamRouteFactory("/"); + var route2 = DownstreamRouteFactory("/test"); + + var resiliencePipeline1 = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route1); + var resiliencePipeline2 = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route2); + + resiliencePipeline1.ShouldNotBe(resiliencePipeline2); + } + + [Fact] + public void Should_build_and_wrap_contains_two_policies() + { + var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); + + var route = DownstreamRouteFactory("/"); + var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); + resiliencePipeline.ShouldNotBeNull(); + + var resiliencePipelineDescriptor = resiliencePipeline.GetPipelineDescriptor(); + resiliencePipelineDescriptor.ShouldNotBeNull(); + resiliencePipelineDescriptor.Strategies.Count.ShouldBe(2); + resiliencePipelineDescriptor.Strategies[0].Options.ShouldBeOfType(); + resiliencePipelineDescriptor.Strategies[1].Options.ShouldBeOfType>(); + } + + [Fact] + public void Should_build_and_contains_one_policy_when_with_exceptions_allowed_before_breaking_is_zero() + { + var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); + + // get route with 0 exceptions allowed before breaking + var route = DownstreamRouteFactory("/", true); + var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); + resiliencePipeline.ShouldNotBeNull(); + + var resiliencePipelineDescriptor = resiliencePipeline.GetPipelineDescriptor(); + resiliencePipelineDescriptor.ShouldNotBeNull(); + resiliencePipelineDescriptor.Strategies.Count.ShouldBe(1); + resiliencePipelineDescriptor.Strategies.Single().Options.ShouldBeOfType(); + } + + [Theory] + [InlineData(HttpStatusCode.InternalServerError)] + [InlineData(HttpStatusCode.NotImplemented)] + [InlineData(HttpStatusCode.BadGateway)] + [InlineData(HttpStatusCode.ServiceUnavailable)] + [InlineData(HttpStatusCode.GatewayTimeout)] + [InlineData(HttpStatusCode.HttpVersionNotSupported)] + [InlineData(HttpStatusCode.VariantAlsoNegotiates)] + [InlineData(HttpStatusCode.InsufficientStorage)] + [InlineData(HttpStatusCode.LoopDetected)] + public async Task Should_throw_broken_circuit_exception_after_two_exceptions(HttpStatusCode errorCode) + { + var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); + + var route = DownstreamRouteFactory("/"); + var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); + + var response = new HttpResponseMessage(errorCode); + await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); + await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); + await Assert.ThrowsAsync(async () => + await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))); + } + + [Fact] + public async Task Should_not_throw_broken_circuit_exception_if_status_code_ok() + { + var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); + + var route = DownstreamRouteFactory("/"); + var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); + + var response = new HttpResponseMessage(HttpStatusCode.OK); + Assert.Equal(HttpStatusCode.OK, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))).StatusCode); + Assert.Equal(HttpStatusCode.OK, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))).StatusCode); + Assert.Equal(HttpStatusCode.OK, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))).StatusCode); + } + + [Fact] + public async Task Should_throw_and_before_delay_should_not_allow_requests() + { + var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); + + var route = DownstreamRouteFactory("/"); + var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); + + var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); + await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); + await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); + await Assert.ThrowsAsync(async () => + await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))); + + await Task.Delay(200); + + await Assert.ThrowsAsync(async () => + await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))); + } + + [Fact] + public async Task Should_throw_but_after_delay_should_allow_one_more_internal_server_error() + { + var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); + + var route = DownstreamRouteFactory("/"); + var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); + + var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); + await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); + await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); + await Assert.ThrowsAsync(async () => + await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))); + + await Task.Delay(6000); + + Assert.Equal(HttpStatusCode.InternalServerError, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))).StatusCode); + } + + [Fact] + public async Task Should_throw_but_after_delay_should_allow_one_more_internal_server_error_and_throw() + { + var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); + + var route = DownstreamRouteFactory("/"); + var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); + + var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); + await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); + await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); + await Assert.ThrowsAsync(async () => + await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))); + + await Task.Delay(6000); + + Assert.Equal(HttpStatusCode.InternalServerError, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))).StatusCode); + await Assert.ThrowsAsync(async () => + await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))); + } + + [Fact] + public async Task Should_throw_but_after_delay_should_allow_one_more_ok_request_and_put_counter_back_to_zero() + { + var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); + + var route = DownstreamRouteFactory("/"); + var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); + + var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); + await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); + await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); + await Assert.ThrowsAsync(async () => + await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))); + + await Task.Delay(10000); + + var response2 = new HttpResponseMessage(HttpStatusCode.OK); + Assert.Equal(HttpStatusCode.OK, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response2))).StatusCode); + await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); + await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); + await Assert.ThrowsAsync(async () => + await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))); + } + + private static PollyQoSResiliencePipelineProvider PollyQoSResiliencePipelineProviderFactory() + { + var loggerFactoryMock = new Mock(); + loggerFactoryMock + .Setup(x => x.CreateLogger()) + .Returns(new Mock().Object); + var resiliencePipelineRegistry = new ResiliencePipelineRegistry(); + + var pollyQoSResiliencePipelineProvider = new PollyQoSResiliencePipelineProvider(loggerFactoryMock.Object, resiliencePipelineRegistry); + return pollyQoSResiliencePipelineProvider; + } + + private static DownstreamRoute DownstreamRouteFactory(string routeTemplate, bool inactiveExceptionsAllowedBeforeBreaking = false) + { + var options = new QoSOptionsBuilder() + .WithTimeoutValue(10000) + .WithExceptionsAllowedBeforeBreaking(inactiveExceptionsAllowedBeforeBreaking ? 0 : 2) + .WithDurationOfBreak(5000) + .Build(); + + var upstreamPath = new UpstreamPathTemplateBuilder() + .WithTemplate(routeTemplate) + .WithContainsQueryString(false) + .WithPriority(1) + .WithOriginalValue(routeTemplate) + .Build(); + + var route = new DownstreamRouteBuilder() + .WithQosOptions(options) + .WithUpstreamPathTemplate(upstreamPath) + .Build(); + + return route; + } +} diff --git a/test/Ocelot.UnitTests/Polly/PollyResiliencePipelineDelegatingHandlerTests.cs b/test/Ocelot.UnitTests/Polly/PollyResiliencePipelineDelegatingHandlerTests.cs new file mode 100644 index 0000000000..f144fe3058 --- /dev/null +++ b/test/Ocelot.UnitTests/Polly/PollyResiliencePipelineDelegatingHandlerTests.cs @@ -0,0 +1,123 @@ +using Microsoft.AspNetCore.Http; +using Moq.Protected; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Logging; +using Ocelot.Provider.Polly; +using Ocelot.Provider.Polly.Interfaces; +using Polly; +using Polly.Retry; +using System.Reflection; + +namespace Ocelot.UnitTests.Polly; + +public class PollyResiliencePipelineDelegatingHandlerTests +{ + private readonly Mock> _pollyQoSResiliencePipelineProviderMock; + private readonly Mock _contextAccessorMock; + private readonly PollyResiliencePipelineDelegatingHandler _sut; + + public PollyResiliencePipelineDelegatingHandlerTests() + { + _pollyQoSResiliencePipelineProviderMock = new Mock>(); + + var loggerFactoryMock = new Mock(); + var loggerMock = new Mock(); + _contextAccessorMock = new Mock(); + + loggerFactoryMock.Setup(x => x.CreateLogger()) + .Returns(loggerMock.Object); + loggerMock.Setup(x => x.LogError(It.IsAny(), It.IsAny())); + + _sut = new PollyResiliencePipelineDelegatingHandler(DownstreamRouteFactory(), _contextAccessorMock.Object, loggerFactoryMock.Object); + } + + [Fact] + public async void SendAsync_OnePolicy() + { + // Arrange + var fakeResponse = new HttpResponseMessage(HttpStatusCode.NoContent); + fakeResponse.Headers.Add("X-Xunit", nameof(SendAsync_OnePolicy)); + + // setup the inner handler for PollyResiliencePipelineDelegatingHandler + var innerHandler = new Mock(); + innerHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(fakeResponse); + _sut.InnerHandler = innerHandler.Object; + + // setup the resilience pipeline eg: retry policy + var resiliencePipeline = new ResiliencePipelineBuilder() + .AddRetry(new RetryStrategyOptions + { + ShouldHandle = new PredicateBuilder().Handle(), + }) + .Build(); + + _pollyQoSResiliencePipelineProviderMock + .Setup(x => x.GetResiliencePipeline(It.IsAny())) + .Returns(resiliencePipeline); + + var httpContext = new Mock(); + httpContext + .Setup(x => x.RequestServices.GetService(typeof(IPollyQoSResiliencePipelineProvider))) + .Returns(_pollyQoSResiliencePipelineProviderMock.Object); + + _contextAccessorMock + .Setup(x => x.HttpContext) + .Returns(httpContext.Object); + + // Act + var actual = await InvokeAsync("SendAsync"); + + // Assert + ShouldHaveXunitHeaderWithNoContent(actual, nameof(SendAsync_OnePolicy)); + + // TODO: do more checks + // check that the pipeline provider was called only once + _pollyQoSResiliencePipelineProviderMock + .Verify(a => a.GetResiliencePipeline(It.IsAny()), times: Times.Once); + _pollyQoSResiliencePipelineProviderMock + .VerifyNoOtherCalls(); + + // this check has no sense anymore + //method.DeclaringType.Name.ShouldBe("IAsyncPolicy`1"); + //method.DeclaringType.ShouldNotBeOfType(); + } + + private async Task InvokeAsync(string methodName) + { + var m = typeof(PollyResiliencePipelineDelegatingHandler).GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic); + var task = (Task)m.Invoke(_sut, new object[] { new HttpRequestMessage(), CancellationToken.None }); + var actual = await task!; + return actual; + } + + private static void ShouldHaveXunitHeaderWithNoContent(HttpResponseMessage actual, string headerName) + { + actual.ShouldNotBeNull(); + actual.StatusCode.ShouldBe(HttpStatusCode.NoContent); + actual.Headers.GetValues("X-Xunit").ShouldContain(headerName); + } + + private static DownstreamRoute DownstreamRouteFactory() + { + var options = new QoSOptionsBuilder() + .WithTimeoutValue(100) + .WithExceptionsAllowedBeforeBreaking(2) + .WithDurationOfBreak(200) + .Build(); + + var upstreamPath = new UpstreamPathTemplateBuilder() + .WithTemplate("/") + .WithContainsQueryString(false) + .WithPriority(1) + .WithOriginalValue("/").Build(); + + var route = new DownstreamRouteBuilder() + .WithQosOptions(options) + .WithUpstreamPathTemplate(upstreamPath).Build(); + + return route; + } +} diff --git a/test/Ocelot.UnitTests/QueryStrings/AddQueriesToRequestTests.cs b/test/Ocelot.UnitTests/QueryStrings/AddQueriesToRequestTests.cs index 83c5b320a9..5c2c3b7948 100644 --- a/test/Ocelot.UnitTests/QueryStrings/AddQueriesToRequestTests.cs +++ b/test/Ocelot.UnitTests/QueryStrings/AddQueriesToRequestTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.QueryStrings { - public class AddQueriesToRequestTests + public class AddQueriesToRequestTests : UnitTest { private readonly AddQueriesToRequest _addQueriesToRequest; private DownstreamRequest _downstreamRequest; diff --git a/test/Ocelot.UnitTests/QueryStrings/ClaimsToQueryStringMiddlewareTests.cs b/test/Ocelot.UnitTests/QueryStrings/ClaimsToQueryStringMiddlewareTests.cs index 731f3fa0ec..bfb0af8aae 100644 --- a/test/Ocelot.UnitTests/QueryStrings/ClaimsToQueryStringMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/QueryStrings/ClaimsToQueryStringMiddlewareTests.cs @@ -13,7 +13,7 @@ namespace Ocelot.UnitTests.QueryStrings { - public class ClaimsToQueryStringMiddlewareTests + public class ClaimsToQueryStringMiddlewareTests : UnitTest { private readonly Mock _addQueries; private readonly Mock _loggerFactory; diff --git a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs deleted file mode 100644 index 0e6c66068c..0000000000 --- a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs +++ /dev/null @@ -1,183 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Caching.Memory; -using Ocelot.Configuration; -using Ocelot.Configuration.Builder; -using Ocelot.Logging; -using Ocelot.Middleware; -using Ocelot.RateLimit; -using Ocelot.RateLimit.Middleware; -using Ocelot.Request.Middleware; - -namespace Ocelot.UnitTests.RateLimit -{ - public class ClientRateLimitMiddlewareTests - { - private readonly IRateLimitCounterHandler _rateLimitCounterHandler; - private readonly Mock _loggerFactory; - private readonly Mock _logger; - private readonly ClientRateLimitMiddleware _middleware; - private readonly RequestDelegate _next; - private DownstreamResponse _downstreamResponse; - private readonly string _url; - - public ClientRateLimitMiddlewareTests() - { - _url = "http://localhost:51879"; - var cacheEntryOptions = new MemoryCacheOptions(); - _rateLimitCounterHandler = new MemoryCacheRateLimitCounterHandler(new MemoryCache(cacheEntryOptions)); - _loggerFactory = new Mock(); - _logger = new Mock(); - _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - _next = context => Task.CompletedTask; - _middleware = new ClientRateLimitMiddleware(_next, _loggerFactory.Object, _rateLimitCounterHandler); - } - - [Fact] - public void should_call_middleware_and_ratelimiting() - { - var upstreamTemplate = new UpstreamPathTemplateBuilder().Build(); - - var downstreamRoute = new DownstreamRouteBuilder() - .WithEnableRateLimiting(true) - .WithRateLimitOptions(new RateLimitOptions(true, "ClientId", () => new List(), false, string.Empty, string.Empty, new RateLimitRule("1s", 100, 3), 429)) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(upstreamTemplate) - .Build(); - - var route = new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build(); - - var downstreamRouteHolder = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(new List(), route); - - this.Given(x => x.WhenICallTheMiddlewareMultipleTimes(2, downstreamRouteHolder)) - .Then(x => x.ThenThereIsNoDownstreamResponse()) - .When(x => x.WhenICallTheMiddlewareMultipleTimes(3, downstreamRouteHolder)) - .Then(x => x.ThenTheResponseIs429()) - .BDDfy(); - } - - [Fact] - public void should_call_middleware_withWhitelistClient() - { - var downstreamRoute = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithEnableRateLimiting(true) - .WithRateLimitOptions( - new RateLimitOptions(true, "ClientId", () => new List { "ocelotclient2" }, false, string.Empty, string.Empty, new RateLimitRule("1s", 100, 3), 429)) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); - - this.Given(x => x.WhenICallTheMiddlewareWithWhiteClient(downstreamRoute)) - .Then(x => x.ThenThereIsNoDownstreamResponse()) - .BDDfy(); - } - - private void WhenICallTheMiddlewareMultipleTimes(int times, Ocelot.DownstreamRouteFinder.DownstreamRouteHolder downstreamRoute) - { - var httpContexts = new List(); - - for (var i = 0; i < times; i++) - { - var httpContext = new DefaultHttpContext - { - Response = - { - Body = new FakeStream(), - }, - }; - httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); - httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); - httpContext.Items.UpsertDownstreamRoute(downstreamRoute); - var clientId = "ocelotclient1"; - var request = new HttpRequestMessage(new HttpMethod("GET"), _url); - httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(request)); - httpContext.Request.Headers.TryAdd("ClientId", clientId); - httpContexts.Add(httpContext); - } - - foreach (var httpContext in httpContexts) - { - _middleware.Invoke(httpContext).GetAwaiter().GetResult(); - var ds = httpContext.Items.DownstreamResponse(); - _downstreamResponse = ds; - } - } - - private void WhenICallTheMiddlewareWithWhiteClient(Ocelot.DownstreamRouteFinder.DownstreamRouteHolder downstreamRoute) - { - var clientId = "ocelotclient2"; - - for (var i = 0; i < 10; i++) - { - var httpContext = new DefaultHttpContext - { - Response = - { - Body = new FakeStream(), - }, - }; - httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); - httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); - httpContext.Items.UpsertDownstreamRoute(downstreamRoute); - var request = new HttpRequestMessage(new HttpMethod("GET"), _url); - request.Headers.Add("ClientId", clientId); - httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(request)); - httpContext.Request.Headers.TryAdd("ClientId", clientId); - _middleware.Invoke(httpContext).GetAwaiter().GetResult(); - var ds = httpContext.Items.DownstreamResponse(); - _downstreamResponse = ds; - } - } - - private void ThenTheResponseIs429() - { - var code = (int)_downstreamResponse.StatusCode; - code.ShouldBe(429); - } - - private void ThenThereIsNoDownstreamResponse() - { - _downstreamResponse.ShouldBeNull(); - } - } - - internal class FakeStream : Stream - { - public override void Flush() - { - //do nothing - //throw new System.NotImplementedException(); - } - - public override int Read(byte[] buffer, int offset, int count) - { - throw new System.NotImplementedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new System.NotImplementedException(); - } - - public override void SetLength(long value) - { - throw new System.NotImplementedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - //do nothing - } - - public override bool CanRead { get; } - public override bool CanSeek { get; } - public override bool CanWrite => true; - public override long Length { get; } - public override long Position { get; set; } - } -} diff --git a/test/Ocelot.UnitTests/RateLimiting/RateLimitingMiddlewareTests.cs b/test/Ocelot.UnitTests/RateLimiting/RateLimitingMiddlewareTests.cs new file mode 100644 index 0000000000..29c3b0deed --- /dev/null +++ b/test/Ocelot.UnitTests/RateLimiting/RateLimitingMiddlewareTests.cs @@ -0,0 +1,218 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Logging; +using Ocelot.Middleware; +using Ocelot.RateLimiting; +using Ocelot.RateLimiting.Middleware; +using Ocelot.Request.Middleware; +using System.Text; +using _DownstreamRouteHolder_ = Ocelot.DownstreamRouteFinder.DownstreamRouteHolder; +using _RateLimiting_ = Ocelot.RateLimiting.RateLimiting; + +namespace Ocelot.UnitTests.RateLimiting; + +public class RateLimitingMiddlewareTests : UnitTest +{ + private readonly IRateLimitStorage _storage; + private readonly Mock _loggerFactory; + private readonly Mock _logger; + private readonly RateLimitingMiddleware _middleware; + private readonly RequestDelegate _next; + private readonly IRateLimiting _rateLimiting; + private readonly List _downstreamResponses; + private readonly string _url; + + public RateLimitingMiddlewareTests() + { + _url = "http://localhost:51879"; + var cacheEntryOptions = new MemoryCacheOptions(); + _storage = new MemoryCacheRateLimitStorage(new MemoryCache(cacheEntryOptions)); + _loggerFactory = new Mock(); + _logger = new Mock(); + _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _next = context => Task.CompletedTask; + _rateLimiting = new _RateLimiting_(_storage); + _middleware = new RateLimitingMiddleware(_next, _loggerFactory.Object, _rateLimiting); + _downstreamResponses = new(); + } + + [Fact] + [Trait("Feat", "37")] + public async Task Should_call_middleware_and_ratelimiting() + { + // Arrange + const long limit = 3L; + var upstreamTemplate = new UpstreamPathTemplateBuilder() + .Build(); + var downstreamRoute = new DownstreamRouteBuilder() + .WithEnableRateLimiting(true) + .WithRateLimitOptions(new( + enableRateLimiting: true, + clientIdHeader: "ClientId", + getClientWhitelist: () => new List(), + disableRateLimitHeaders: false, + quotaExceededMessage: "Exceeding!", + rateLimitCounterPrefix: string.Empty, + new RateLimitRule("1s", 100.0D, limit), + (int)HttpStatusCode.TooManyRequests)) + .WithUpstreamHttpMethod(new() { "Get" }) + .WithUpstreamPathTemplate(upstreamTemplate) + .Build(); + var route = new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) + .WithUpstreamHttpMethod(new() { "Get" }) + .Build(); + var downstreamRouteHolder = new _DownstreamRouteHolder_(new(), route); + + // Act, Assert + await WhenICallTheMiddlewareMultipleTimes(limit, downstreamRouteHolder); + _downstreamResponses.ForEach(dsr => dsr.ShouldBeNull()); + + // Act, Assert: the next request should fail + await WhenICallTheMiddlewareMultipleTimes(3, downstreamRouteHolder); + _downstreamResponses.ShouldNotBeNull(); + for (int i = 0; i < _downstreamResponses.Count; i++) + { + var response = _downstreamResponses[i].ShouldNotBeNull(); + response.StatusCode.ShouldBe(HttpStatusCode.TooManyRequests, $"Downstream Response no is {i}"); + var body = await response.Content.ReadAsStringAsync(); + body.ShouldBe("Exceeding!"); + } + } + + [Fact] + [Trait("Feat", "37")] + public async Task Should_call_middleware_withWhitelistClient() + { + // Arrange + var route = new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithEnableRateLimiting(true) + .WithRateLimitOptions(new( + enableRateLimiting: true, + clientIdHeader: "ClientId", + getClientWhitelist: () => new List { "ocelotclient2" }, + disableRateLimitHeaders: false, + quotaExceededMessage: "Exceeding!", + rateLimitCounterPrefix: string.Empty, + new RateLimitRule("1s", 100.0D, 3), + (int)HttpStatusCode.TooManyRequests)) + .WithUpstreamHttpMethod(new() { "Get" }) + .Build()) + .WithUpstreamHttpMethod(new() { "Get" }) + .Build(); + var downstreamRoute = new _DownstreamRouteHolder_(new(), route); + + // Act + await WhenICallTheMiddlewareWithWhiteClient(downstreamRoute); + + // Assert + _downstreamResponses.ForEach(dsr => dsr.ShouldBeNull()); + } + + [Fact] + [Trait("Bug", "1590")] + public async Task MiddlewareInvoke_PeriodTimespanValueIsGreaterThanPeriod_StatusNotEqualTo429() + { + // Arrange + const long limit = 100L; + var upstreamTemplate = new UpstreamPathTemplateBuilder() + .Build(); + var downstreamRoute = new DownstreamRouteBuilder() + .WithEnableRateLimiting(true) + .WithRateLimitOptions(new( + enableRateLimiting: true, + clientIdHeader: "ClientId", + getClientWhitelist: () => new List(), + disableRateLimitHeaders: false, + quotaExceededMessage: "Exceeding!", + rateLimitCounterPrefix: string.Empty, + new RateLimitRule("1s", 30.0D, limit), // bug scenario + (int)HttpStatusCode.TooManyRequests)) + .WithUpstreamHttpMethod(new() { "Get" }) + .WithUpstreamPathTemplate(upstreamTemplate) + .Build(); + var route = new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) + .WithUpstreamHttpMethod(new() { "Get" }) + .Build(); + var downstreamRouteHolder = new _DownstreamRouteHolder_(new(), route); + + // Act, Assert: 100 requests must be successful + var contexts = await WhenICallTheMiddlewareMultipleTimes(limit, downstreamRouteHolder); // make 100 requests, but not exceed the limit + _downstreamResponses.ForEach(dsr => dsr.ShouldBeNull()); + contexts.ForEach(ctx => + { + ctx.ShouldNotBeNull(); + ctx.Items.Errors().ShouldNotBeNull().ShouldBeEmpty(); // no errors + ctx.Response.StatusCode.ShouldBe((int)HttpStatusCode.OK); // not 429 aka TooManyRequests + }); + + // Act, Assert: the next 101st request should fail + contexts = await WhenICallTheMiddlewareMultipleTimes(1, downstreamRouteHolder); + _downstreamResponses.ShouldNotBeNull(); + var ds = _downstreamResponses.SingleOrDefault().ShouldNotBeNull(); + ds.StatusCode.ShouldBe(HttpStatusCode.TooManyRequests, $"Downstream Response no {limit + 1}"); + var body = await ds.Content.ReadAsStringAsync(); + body.ShouldBe("Exceeding!"); + contexts[0].Items.Errors().ShouldNotBeNull().ShouldNotBeEmpty(); // having errors + contexts[0].Items.Errors().Single().HttpStatusCode.ShouldBe((int)HttpStatusCode.TooManyRequests); + } + + private async Task> WhenICallTheMiddlewareMultipleTimes(long times, _DownstreamRouteHolder_ downstreamRoute) + { + var contexts = new List(); + _downstreamResponses.Clear(); + for (var i = 0; i < times; i++) + { + var context = new DefaultHttpContext(); + var stream = GetFakeStream($"{i}"); + context.Response.Body = stream; + context.Response.RegisterForDispose(stream); + context.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); + context.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); + context.Items.UpsertDownstreamRoute(downstreamRoute); + var request = new HttpRequestMessage(new HttpMethod("GET"), _url); + context.Items.UpsertDownstreamRequest(new DownstreamRequest(request)); + context.Request.Headers.TryAdd("ClientId", "ocelotclient1"); + contexts.Add(context); + + await _middleware.Invoke(context); + + _downstreamResponses.Add(context.Items.DownstreamResponse()); + } + + return contexts; + } + + private static Stream GetFakeStream(string str) + { + byte[] data = Encoding.ASCII.GetBytes(str); + return new MemoryStream(data, 0, data.Length); + } + + private async Task WhenICallTheMiddlewareWithWhiteClient(_DownstreamRouteHolder_ downstreamRoute) + { + const string ClientId = "ocelotclient2"; + for (var i = 0; i < 10; i++) + { + var context = new DefaultHttpContext(); + var stream = GetFakeStream($"{i}"); + context.Response.Body = stream; + context.Response.RegisterForDispose(stream); + context.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); + context.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); + context.Items.UpsertDownstreamRoute(downstreamRoute); + var request = new HttpRequestMessage(new HttpMethod("GET"), _url); + request.Headers.Add("ClientId", ClientId); + context.Items.UpsertDownstreamRequest(new DownstreamRequest(request)); + context.Request.Headers.TryAdd("ClientId", ClientId); + + await _middleware.Invoke(context); + + _downstreamResponses.Add(context.Items.DownstreamResponse()); + } + } +} diff --git a/test/Ocelot.UnitTests/RateLimiting/RateLimitingTests.cs b/test/Ocelot.UnitTests/RateLimiting/RateLimitingTests.cs new file mode 100644 index 0000000000..a4eb4738e4 --- /dev/null +++ b/test/Ocelot.UnitTests/RateLimiting/RateLimitingTests.cs @@ -0,0 +1,268 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.RateLimiting; +using System.Runtime.CompilerServices; +using _RateLimiting_ = Ocelot.RateLimiting.RateLimiting; + +namespace Ocelot.UnitTests.RateLimiting; + +public sealed class RateLimitingTests +{ + private readonly Mock _storage; + private readonly _RateLimiting_ _sut; + + public RateLimitingTests() + { + _storage = new(); + _sut = new(_storage.Object); + } + + [Theory] + [Trait("Feat", "37")] + [InlineData(null)] + [InlineData("")] + public void ToTimespan_EmptyValue_ShouldReturnZero(string empty) + { + // Arrange, Act + var actual = _sut.ToTimespan(empty); + + // Assert + Assert.Equal(TimeSpan.Zero, actual); + } + + [Theory] + [Trait("Feat", "37")] + [InlineData("1a")] + [InlineData("2unknown")] + public void ToTimespan_UnknownType_ShouldThrowFormatException(string timespan) + { + // Arrange, Act, Assert + Assert.Throws( + () => _sut.ToTimespan(timespan)); + } + + [Theory] + [Trait("Feat", "37")] + [InlineData("1s", 1 * TimeSpan.TicksPerSecond)] + [InlineData("2m", 2 * TimeSpan.TicksPerMinute)] + [InlineData("3h", 3 * TimeSpan.TicksPerHour)] + [InlineData("4d", 4 * TimeSpan.TicksPerDay)] + public void ToTimespan_KnownType_HappyPath(string timespan, long ticks) + { + // Arrange + var expected = TimeSpan.FromTicks(ticks); + + // Act + var actual = _sut.ToTimespan(timespan); + + // Assert + Assert.Equal(expected, actual); + } + + [Fact] + [Trait("PR", "1592")] + public void Count_NoEntry_StartCounting() + { + // Arrange + RateLimitCounter? arg1 = null; // No Entry + RateLimitRule arg2 = null; + + // Act + RateLimitCounter actual = _sut.Count(arg1, arg2); + + // Assert + Assert.Equal(1L, actual.TotalRequests); + Assert.True(DateTime.UtcNow - actual.StartedAt < TimeSpan.FromSeconds(1.0D)); + } + + [Fact] + [Trait("PR", "1592")] + public void Count_EntryHasNotExpired_IncrementedRequestCount() + { + // Arrange + long total = 2; + RateLimitCounter? arg1 = new RateLimitCounter(DateTime.UtcNow, null, total); // entry has not expired + RateLimitRule arg2 = new("1s", 1.0D, total + 1); // with not exceeding limit + + // Act + RateLimitCounter actual = _sut.Count(arg1, arg2); + + // Assert + Assert.Equal(total + 1, actual.TotalRequests); // incremented request count + Assert.Equal(arg1.Value.StartedAt, actual.StartedAt); // starting point has not changed + } + + [Fact] + [Trait("PR", "1592")] + public void Count_EntryHasNotExpiredAndExceedingLimit_IncrementedRequestCountWithRenewedStartMoment() + { + // Arrange + long total = 2; + RateLimitCounter? arg1 = new RateLimitCounter(DateTime.UtcNow, null, total); // entry has not expired + RateLimitRule arg2 = new("1s", 1.0D, 1L); + + // Act + RateLimitCounter actual = _sut.Count(arg1, arg2); + + // Assert + Assert.Equal(total + 1, actual.TotalRequests); // incremented request count + Assert.InRange(actual.StartedAt, arg1.Value.StartedAt, DateTime.UtcNow); // starting point has renewed and it is between StartedAt and Now + } + + [Fact] + [Trait("PR", "1592")] + public void Count_RateLimitExceeded_StartedCounting() + { + // Arrange + long total = 3, limit = total - 1; + TimeSpan periodTimespan = TimeSpan.FromSeconds(1.0D); + DateTime startedAt = DateTime.UtcNow.AddSeconds(-2.0), // 2 secs ago + exceededAt = startedAt + periodTimespan; // 1 second ago + RateLimitCounter? arg1 = new RateLimitCounter(startedAt, exceededAt, total); // Entry has expired + RateLimitRule arg2 = new("1s", periodTimespan.TotalSeconds, limit); // rate limit exceeded + + // Act + RateLimitCounter actual = _sut.Count(arg1, arg2); + + // Assert + Assert.Equal(1L, actual.TotalRequests); // started counting, the counter was changed + Assert.InRange(actual.StartedAt, arg1.Value.ExceededAt.Value, DateTime.UtcNow); // starting point has renewed and it is between exceededAt and Now + } + + [Fact] + [Trait("PR", "1592")] + public void Count_RateLimitNotExceededAndPeriodIsElapsed_StartedCountingByDefault() + { + // Arrange + long total = 3, limit = 3; + RateLimitCounter? arg1 = new RateLimitCounter(DateTime.UtcNow.AddSeconds(-2.0), null, total); // Entry has expired + RateLimitRule arg2 = new("1s", 1.0D, limit); // Rate limit not exceeded + + // Act + RateLimitCounter actual = _sut.Count(arg1, arg2); + + // Assert + Assert.Equal(1L, actual.TotalRequests); // started counting + Assert.True(DateTime.UtcNow - actual.StartedAt < TimeSpan.FromSeconds(1.0D)); // started now + } + + [Fact] + [Trait("PR", "1592")] + public void ProcessRequest_RateLimitExceededAndBanPeriodElapsed_StartedCounting() + { + // Arrange + const double periodTimespan = 2.0D; + const int millisecondsBeforeAfterEnding = 100; // current processing time of unit test should not take more 100 ms + DateTime now = DateTime.UtcNow, + startedAt = now.AddSeconds(-3).AddMilliseconds(millisecondsBeforeAfterEnding); + DateTime? exceededAt = null; + long totalRequests = 2L; + TimeSpan expiration = TimeSpan.Zero; + + var (identity, options) = SetupProcessRequest("3s", periodTimespan, totalRequests, + () => new RateLimitCounter(startedAt, exceededAt, totalRequests), + (value) => expiration = value); + + // Act 1 + var counter = _sut.ProcessRequest(identity, options); + + // Assert 1 + Assert.Equal(3L, counter.TotalRequests); // old counting -> 3 + Assert.Equal(startedAt, counter.StartedAt); // starting point was not changed + Assert.NotNull(counter.ExceededAt); // exceeded + Assert.Equal(DateTime.UtcNow.Second, counter.ExceededAt.Value.Second); // exceeded now, in the same second + + // Arrange 2 + TimeSpan shift = TimeSpan.FromSeconds(periodTimespan); // don't wait, just move to future + startedAt = counter.StartedAt - shift; // move to past + exceededAt = counter.ExceededAt - shift; // move to past + totalRequests = counter.TotalRequests; // 3 + + // Act 2 + var actual = _sut.ProcessRequest(identity, options); + + // Assert + Assert.Equal(1L, actual.TotalRequests); // started counting + Assert.InRange(actual.StartedAt, now, DateTime.UtcNow); // starting point has renewed and it is between test starting and Now + Assert.Null(actual.ExceededAt); + _storage.Verify(x => x.Remove(It.IsAny()), + Times.Never()); // Once()? Seems Remove is never called because of renewing + _storage.Verify(x => x.Get(It.IsAny()), + Times.Exactly(2)); + _storage.Verify(x => x.Set(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Exactly(2)); + Assert.Equal(TimeSpan.FromSeconds(3), expiration); + } + + private (ClientRequestIdentity Identity, RateLimitOptions Options) SetupProcessRequest(string period, double periodTimespan, long limit, + Func counterFactory, Action expirationAction, [CallerMemberName] string testName = "") + { + ClientRequestIdentity identity = new(nameof(RateLimitingTests), "/" + testName, HttpMethods.Get); + RateLimitOptions options = new RateLimitOptionsBuilder() + .WithEnableRateLimiting(true) + .WithRateLimitCounterPrefix(nameof(_RateLimiting_.ProcessRequest)) + .WithRateLimitRule(new RateLimitRule(period, periodTimespan, limit)) + .Build(); + _storage.Setup(x => x.Get(It.IsAny())) + .Returns(counterFactory); // counter value factory + _storage.Setup(x => x.Remove(It.IsAny())) + .Verifiable(); + expirationAction?.Invoke(TimeSpan.Zero); + _storage.Setup(x => x.Set(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((id, counter, expirationTime) => expirationAction?.Invoke(expirationTime)) + .Verifiable(); + return (identity, options); + } + + [Fact] + [Trait("Bug", "1590")] + public void ProcessRequest_PeriodTimespanValueIsGreaterThanPeriod_ExpectedBehaviorAndExpirationInPeriod() + { + // Arrange: user scenario + const string period = "1s"; + const double periodTimespan = 30.0D; // seconds + const long limit = 100L, requestsPerSecond = 20L; + + // Arrange: setup + DateTime? startedAt = null; + TimeSpan expiration = TimeSpan.Zero; + long total = 1L, count = requestsPerSecond; + RateLimitCounter? current = null; + var (identity, options) = SetupProcessRequest(period, periodTimespan, limit, + () => current, + (value) => expiration = value); + + // Arrange 20 requests per period (1 sec) + var periodSeconds = TimeSpan.FromSeconds(double.Parse(period[0].ToString())); + var periodMilliseconds = periodSeconds.TotalMilliseconds; + int delay = (int)((periodMilliseconds - 200) / requestsPerSecond); // 20 requests per 1 second + + while (count > 0L) + { + // Act + var actual = _sut.ProcessRequest(identity, options); + + // life hack for the 1st request + if (count == requestsPerSecond) + { + startedAt = actual.StartedAt; // for the 1st request get expected value + } + + // Assert + Assert.True(actual.TotalRequests < limit); + actual.TotalRequests.ShouldBe(total++, $"Count is {count}"); + Assert.Equal(startedAt, actual.StartedAt); // starting point is not changed + Assert.Null(actual.ExceededAt); // no exceeding at all + Assert.Equal(periodSeconds, expiration); // expiration in the period + + // Arrange: next micro test + current = actual; + Thread.Sleep(delay); + count--; + } + + Assert.NotEqual(TimeSpan.FromSeconds(periodTimespan), expiration); // Not ban period expiration + Assert.Equal(periodSeconds, expiration); // last 20th request was in counting period + } +} diff --git a/test/Ocelot.UnitTests/Repository/ScopedRequestDataRepositoryTests.cs b/test/Ocelot.UnitTests/Repository/ScopedRequestDataRepositoryTests.cs index 33b7970257..ba6e2c9a37 100644 --- a/test/Ocelot.UnitTests/Repository/ScopedRequestDataRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Repository/ScopedRequestDataRepositoryTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Repository { - public class ScopedRequestDataRepositoryTests + public class ScopedRequestDataRepositoryTests : UnitTest { private readonly IRequestScopedDataRepository _requestScopedDataRepository; private readonly IHttpContextAccessor _httpContextAccesor; diff --git a/test/Ocelot.UnitTests/Request/Creator/DownstreamRequestCreatorTests.cs b/test/Ocelot.UnitTests/Request/Creator/DownstreamRequestCreatorTests.cs index e61811314e..7426b77aa8 100644 --- a/test/Ocelot.UnitTests/Request/Creator/DownstreamRequestCreatorTests.cs +++ b/test/Ocelot.UnitTests/Request/Creator/DownstreamRequestCreatorTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Request.Creator { - public class DownstreamRequestCreatorTests + public class DownstreamRequestCreatorTests : UnitTest { private readonly Mock _framework; private readonly DownstreamRequestCreator _downstreamRequestCreator; diff --git a/test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs b/test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs index 854b70e6df..ed19d1aae3 100644 --- a/test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Request; -public class DownstreamRequestInitialiserMiddlewareTests +public class DownstreamRequestInitialiserMiddlewareTests : UnitTest { private readonly DownstreamRequestInitialiserMiddleware _middleware; private readonly HttpContext _httpContext; diff --git a/test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs b/test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs index 328fee6e5e..6f06d0295b 100644 --- a/test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs +++ b/test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs @@ -5,10 +5,11 @@ using Ocelot.Request.Mapper; using System.Security.Cryptography; using System.Text; +using Microsoft.Net.Http.Headers; namespace Ocelot.UnitTests.Request.Mapper; -public class RequestMapperTests +public class RequestMapperTests : UnitTest { private readonly HttpRequest _inputRequest; private readonly RequestMapper _requestMapper; @@ -109,15 +110,61 @@ public void Should_handle_no_headers() .BDDfy(); } - [Fact] - public void Should_map_content() + [Theory] + [Trait("PR", "1972")] + [InlineData("GET")] + [InlineData("POST")] + public void Should_map_content(string method) { this.Given(_ => GivenTheInputRequestHasContent("This is my content")) - .And(_ => GivenTheInputRequestHasMethod("GET")) + .And(_ => GivenTheInputRequestHasMethod(method)) + .And(_ => GivenTheInputRequestHasAValidUri()) + .And(_ => GivenTheDownstreamRoute()) + .When(_ => WhenMapped()) + .And(_ => ThenTheMappedRequestHasContent("This is my content")) + .And(_ => ThenTheMappedRequestHasContentLength("This is my content".Length)) + .BDDfy(); + } + + [Fact] + [Trait("PR", "1972")] + public void Should_map_chucked_content() + { + this.Given(_ => GivenTheInputRequestHasChunkedContent("This", " is my content")) + .And(_ => GivenTheInputRequestHasMethod("POST")) .And(_ => GivenTheInputRequestHasAValidUri()) .And(_ => GivenTheDownstreamRoute()) .When(_ => WhenMapped()) .And(_ => ThenTheMappedRequestHasContent("This is my content")) + .And(_ => ThenTheMappedRequestHasNoContentLength()) + .BDDfy(); + } + + [Fact] + [Trait("PR", "1972")] + public void Should_map_empty_content() + { + this.Given(_ => GivenTheInputRequestHasContent("")) + .And(_ => GivenTheInputRequestHasMethod("POST")) + .And(_ => GivenTheInputRequestHasAValidUri()) + .And(_ => GivenTheDownstreamRoute()) + .When(_ => WhenMapped()) + .And(_ => ThenTheMappedRequestHasContent("")) + .And(_ => ThenTheMappedRequestHasContentLength(0)) + .BDDfy(); + } + + [Fact] + [Trait("PR", "1972")] + public void Should_map_empty_chucked_content() + { + this.Given(_ => GivenTheInputRequestHasChunkedContent()) + .And(_ => GivenTheInputRequestHasMethod("POST")) + .And(_ => GivenTheInputRequestHasAValidUri()) + .And(_ => GivenTheDownstreamRoute()) + .When(_ => WhenMapped()) + .And(_ => ThenTheMappedRequestHasContent("")) + .And(_ => ThenTheMappedRequestHasNoContentLength()) .BDDfy(); } @@ -159,8 +206,9 @@ public void Should_handle_no_content_length() [Fact] public void Should_map_content_headers() - { - var md5Bytes = MD5.HashData("some md5"u8.ToArray()); + { + var bytes = Encoding.UTF8.GetBytes("some md5"); + var md5Bytes = MD5.HashData(bytes); this.Given(_ => GivenTheInputRequestHasContent("This is my content")) .And(_ => GivenTheContentTypeIs("application/json")) @@ -393,9 +441,18 @@ private void GivenTheInputRequestHasNoHeaders() private void GivenTheInputRequestHasContent(string content) { + _inputRequest.ContentLength = content.Length; _inputRequest.Body = new MemoryStream(Encoding.UTF8.GetBytes(content)); } + private void GivenTheInputRequestHasChunkedContent(params string[] chunks) + { + // ASP.Net Core decodes chucked streams, so that the input request just sees the decoded data + // Because of that, we just give a stream with the concatenated chunks to the test + _inputRequest.Body = new MemoryStream(Encoding.UTF8.GetBytes(string.Join("", chunks))); + _inputRequest.Headers.TransferEncoding = "chunked"; + } + private void GivenTheInputRequestHasNullContent() { _inputRequest.Body = null!; @@ -448,6 +505,17 @@ private void ThenTheMappedRequestHasContent(string expectedContent) _mappedRequest.Content.ReadAsStringAsync().GetAwaiter().GetResult().ShouldBe(expectedContent); } + private void ThenTheMappedRequestHasContentLength(long expectedLength) + { + Assert.NotNull(_mappedRequest.Content); + _mappedRequest.Content.Headers.ContentLength.ShouldBe(expectedLength); + } + + private void ThenTheMappedRequestHasNoContentLength() + { + _mappedRequest.Headers.TryGetValues(HeaderNames.ContentLength, out _).ShouldBeFalse(); + } + private void ThenTheMappedRequestHasNoContent() { _mappedRequest.Content.ShouldBeNull(); diff --git a/test/Ocelot.UnitTests/Request/Mapper/StreamHttpContentTests.cs b/test/Ocelot.UnitTests/Request/Mapper/StreamHttpContentTests.cs index b8ae8a50f4..105bb85de3 100644 --- a/test/Ocelot.UnitTests/Request/Mapper/StreamHttpContentTests.cs +++ b/test/Ocelot.UnitTests/Request/Mapper/StreamHttpContentTests.cs @@ -35,8 +35,9 @@ public async Task Copy_body_to_stream_with_unknown_length_and_stream_content_sho var bytes = Encoding.UTF8.GetBytes(PayLoad); using var inputStream = new MemoryStream(bytes); using var outputStream = new MemoryStream(); - await CopyAsyncTest(new StreamHttpContent(_httpContext), - [inputStream, outputStream, StreamHttpContent.UnknownLength, false, CancellationToken.None]); + await CopyAsyncTest( + new StreamHttpContent(_httpContext), + new object[] { inputStream, outputStream, StreamHttpContent.UnknownLength, false, CancellationToken.None }); inputStream.Position = 0; outputStream.Position = 0; var result = Encoding.UTF8.GetString(outputStream.ToArray()); @@ -49,8 +50,9 @@ public async Task Copy_body_to_stream_with_body_length_and_stream_content_should var bytes = Encoding.UTF8.GetBytes(PayLoad); using var inputStream = new MemoryStream(bytes); using var outputStream = new MemoryStream(); - await CopyAsyncTest(new StreamHttpContent(_httpContext), - [inputStream, outputStream, bytes.Length, false, CancellationToken.None]); + await CopyAsyncTest( + new StreamHttpContent(_httpContext), + new object[] { inputStream, outputStream, bytes.Length, false, CancellationToken.None }); inputStream.Position = 0; outputStream.Position = 0; var result = Encoding.UTF8.GetString(outputStream.ToArray()); @@ -64,8 +66,9 @@ public async Task Should_throw_if_passed_body_length_does_not_match_real_body_le using var inputStream = new MemoryStream(bytes); using var outputStream = new MemoryStream(); await Assert.ThrowsAsync(async () => - await CopyAsyncTest(new StreamHttpContent(_httpContext), - [inputStream, outputStream, 10, false, CancellationToken.None])); + await CopyAsyncTest( + new StreamHttpContent(_httpContext), + new object[] { inputStream, outputStream, 10, false, CancellationToken.None })); } private StreamHttpContent StreamHttpContentFactory() diff --git a/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs b/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs index 641f9c9cc3..bac14d8577 100644 --- a/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.RequestId { - public class RequestIdMiddlewareTests + public class RequestIdMiddlewareTests : UnitTest { private readonly HttpRequestMessage _downstreamRequest; private string _value; diff --git a/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs index a6c74c1150..dde971d693 100644 --- a/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Requester { - public class DelegatingHandlerHandlerProviderFactoryTests + public class DelegatingHandlerHandlerProviderFactoryTests : UnitTest { private DelegatingHandlerHandlerFactory _factory; private readonly Mock _loggerFactory; diff --git a/test/Ocelot.UnitTests/Requester/HttpExceptionToErrorMapperTests.cs b/test/Ocelot.UnitTests/Requester/HttpExceptionToErrorMapperTests.cs index 89eab729b4..d0742921d4 100644 --- a/test/Ocelot.UnitTests/Requester/HttpExceptionToErrorMapperTests.cs +++ b/test/Ocelot.UnitTests/Requester/HttpExceptionToErrorMapperTests.cs @@ -52,7 +52,7 @@ public void Should_return_request_canceled_for_subtype() [Fact] public void Should_return_error_from_mapper() { - var errorMapping = new Dictionary> + IDictionary> errorMapping = new Dictionary> { {typeof(TaskCanceledException), e => new AnyError()}, }; diff --git a/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs index 3dda1467c3..9060494b66 100644 --- a/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Requester { - public class HttpRequesterMiddlewareTests + public class HttpRequesterMiddlewareTests : UnitTest { private readonly Mock _requester; private Response _response; @@ -34,7 +34,7 @@ public HttpRequesterMiddlewareTests() public void should_call_services_correctly() { this.Given(x => x.GivenTheRequestIs()) - .And(x => x.GivenTheRequesterReturns(new OkResponse(new HttpResponseMessage(System.Net.HttpStatusCode.OK)))) + .And(x => x.GivenTheRequesterReturns(new OkResponse(new HttpResponseMessage(HttpStatusCode.OK)))) .When(x => x.WhenICallTheMiddleware()) .Then(x => x.ThenTheDownstreamResponseIsSet()) .Then(x => InformationIsLogged()) @@ -56,12 +56,38 @@ public void should_log_downstream_internal_server_error() { this.Given(x => x.GivenTheRequestIs()) .And(x => x.GivenTheRequesterReturns( - new OkResponse(new HttpResponseMessage(System.Net.HttpStatusCode.InternalServerError)))) + new OkResponse(new HttpResponseMessage(HttpStatusCode.InternalServerError)))) .When(x => x.WhenICallTheMiddleware()) .Then(x => x.WarningIsLogged()) .BDDfy(); } + + [Theory] + [Trait("Bug", "1953")] + [InlineData(HttpStatusCode.OK)] + [InlineData(HttpStatusCode.PermanentRedirect)] + public void Should_LogInformation_when_status_is_less_than_BadRequest(HttpStatusCode status) + { + this.Given(x => x.GivenTheRequestIs()) + .And(x => x.GivenTheRequesterReturns(new OkResponse(new HttpResponseMessage(status)))) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.InformationIsLogged()) + .BDDfy(); + } + [Theory] + [Trait("Bug", "1953")] + [InlineData(HttpStatusCode.BadRequest)] + [InlineData(HttpStatusCode.NotFound)] + public void Should_LogWarning_when_status_is_BadRequest_or_greater(HttpStatusCode status) + { + this.Given(x => x.GivenTheRequestIs()) + .And(x => x.GivenTheRequesterReturns(new OkResponse(new HttpResponseMessage(status)))) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.WarningIsLogged()) + .BDDfy(); + } + private void ThenTheErrorIsSet() { _httpContext.Items.Errors().Count.ShouldBeGreaterThan(0); @@ -103,18 +129,14 @@ private void ThenTheDownstreamResponseIsSet() private void WarningIsLogged() { _logger.Verify( - x => x.LogWarning( - It.IsAny>() - ), + x => x.LogWarning(It.IsAny>()), Times.Once); } private void InformationIsLogged() { _logger.Verify( - x => x.LogInformation( - It.IsAny>() - ), + x => x.LogInformation(It.IsAny>()), Times.Once); } } diff --git a/test/Ocelot.UnitTests/Requester/MessageInvokerPoolTests.cs b/test/Ocelot.UnitTests/Requester/MessageInvokerPoolTests.cs index fd4eb7d4da..57e3b803a8 100644 --- a/test/Ocelot.UnitTests/Requester/MessageInvokerPoolTests.cs +++ b/test/Ocelot.UnitTests/Requester/MessageInvokerPoolTests.cs @@ -14,7 +14,7 @@ namespace Ocelot.UnitTests.Requester; [Trait("PR", "1824")] -public class MessageInvokerPoolTests +public class MessageInvokerPoolTests : UnitTest { private DownstreamRoute _downstreamRoute1; private DownstreamRoute _downstreamRoute2; @@ -321,7 +321,7 @@ private Mock GetHandlerFactory() { var handlerFactory = new Mock(); handlerFactory.Setup(x => x.Get(It.IsAny())) - .Returns(new OkResponse>>([])); + .Returns(new OkResponse>>(new())); return handlerFactory; } @@ -333,7 +333,7 @@ private DownstreamRoute DownstreamRouteFactory(string path) .WithLoadBalancerKey(string.Empty) .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue(string.Empty).Build()) .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false, false, 10, TimeSpan.FromSeconds(120))) - .WithUpstreamHttpMethod(["Get"]) + .WithUpstreamHttpMethod(new() { "Get" }) .Build(); return downstreamRoute; diff --git a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs index 32c5c8a33f..da55e17525 100644 --- a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs +++ b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs @@ -3,7 +3,7 @@ namespace Ocelot.UnitTests.Responder { - public class ErrorsToHttpStatusCodeMapperTests + public class ErrorsToHttpStatusCodeMapperTests : UnitTest { private readonly IErrorsToHttpStatusCodeMapper _codeMapper; private int _result; @@ -81,7 +81,13 @@ public void should_return_bad_gateway_error(OcelotErrorCode errorCode) public void should_return_not_found(OcelotErrorCode errorCode) { ShouldMapErrorToStatusCode(errorCode, HttpStatusCode.NotFound); - } + } + + [Fact] + public void should_return_request_entity_too_large() + { + ShouldMapErrorsToStatusCode(new() { OcelotErrorCode.PayloadTooLargeError }, HttpStatusCode.RequestEntityTooLarge); + } [Fact] public void AuthenticationErrorsHaveHighestPriority() @@ -128,7 +134,7 @@ public void check_we_have_considered_all_errors_in_these_tests() // If this test fails then it's because the number of error codes has changed. // You should make the appropriate changes to the test cases here to ensure // they cover all the error codes, and then modify this assertion. - Enum.GetNames(typeof(OcelotErrorCode)).Length.ShouldBe(41, "Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?"); + Enum.GetNames(typeof(OcelotErrorCode)).Length.ShouldBe(42, "Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?"); } private void ShouldMapErrorToStatusCode(OcelotErrorCode errorCode, HttpStatusCode expectedHttpStatusCode) diff --git a/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs b/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs index bde95def8a..5a8bc5644d 100644 --- a/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.Responder { - public class ResponderMiddlewareTests + public class ResponderMiddlewareTests : UnitTest { private readonly Mock _responder; private readonly Mock _codeMapper; diff --git a/test/Ocelot.UnitTests/Security/IPSecurityPolicyTests.cs b/test/Ocelot.UnitTests/Security/IPSecurityPolicyTests.cs index bdc86842d4..f32d46bb18 100644 --- a/test/Ocelot.UnitTests/Security/IPSecurityPolicyTests.cs +++ b/test/Ocelot.UnitTests/Security/IPSecurityPolicyTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Security { - public class IPSecurityPolicyTests + public class IPSecurityPolicyTests : UnitTest { private readonly DownstreamRouteBuilder _downstreamRouteBuilder; private readonly IPSecurityPolicy _ipSecurityPolicy; diff --git a/test/Ocelot.UnitTests/Security/SecurityMiddlewareTests.cs b/test/Ocelot.UnitTests/Security/SecurityMiddlewareTests.cs index e8310bb922..c24ccffc20 100644 --- a/test/Ocelot.UnitTests/Security/SecurityMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Security/SecurityMiddlewareTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Security { - public class SecurityMiddlewareTests + public class SecurityMiddlewareTests : UnitTest { private readonly List> _securityPolicyList; private readonly Mock _loggerFactory; diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs index 1de3f5cea9..ad1f7bb724 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs @@ -3,7 +3,7 @@ namespace Ocelot.UnitTests.ServiceDiscovery { - public class ConfigurationServiceProviderTests + public class ConfigurationServiceProviderTests : UnitTest { private ConfigurationServiceProvider _serviceProvider; private List _result; diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs index 88d33f89ce..8f416b2d42 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.ServiceDiscovery { - public class ServiceDiscoveryProviderFactoryTests + public class ServiceDiscoveryProviderFactoryTests : UnitTest { private ServiceProviderConfiguration _serviceConfig; private Response _result; diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceFabricServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceFabricServiceDiscoveryProviderTests.cs index 0836afffcd..1032cbac52 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceFabricServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceFabricServiceDiscoveryProviderTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.ServiceDiscovery { - public class ServiceFabricServiceDiscoveryProviderTests + public class ServiceFabricServiceDiscoveryProviderTests : UnitTest { private ServiceFabricServiceDiscoveryProvider _provider; private ServiceFabricConfiguration _config; diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceRegistryTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceRegistryTests.cs index 22e6dc3160..33670a0dd0 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceRegistryTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceRegistryTests.cs @@ -3,7 +3,7 @@ // nothing in use namespace Ocelot.UnitTests.ServiceDiscovery { - public class ServiceRegistryTests + public class ServiceRegistryTests : UnitTest { private Service _service; private List _services; diff --git a/test/Ocelot.UnitTests/UnitTest.cs b/test/Ocelot.UnitTests/UnitTest.cs new file mode 100644 index 0000000000..a50782f5a7 --- /dev/null +++ b/test/Ocelot.UnitTests/UnitTest.cs @@ -0,0 +1,15 @@ +using TestStack.BDDfy.Configuration; + +namespace Ocelot.UnitTests; + +public class UnitTest +{ + public UnitTest() + { + Configurator.Processors.ConsoleReport.Disable(); + } + + protected readonly Guid _testId = Guid.NewGuid(); + + protected string TestID { get => _testId.ToString("N"); } +} diff --git a/test/Ocelot.UnitTests/WebSockets/WebSocketsProxyMiddlewareTests.cs b/test/Ocelot.UnitTests/WebSockets/WebSocketsProxyMiddlewareTests.cs index d5190ce018..42af9716d9 100644 --- a/test/Ocelot.UnitTests/WebSockets/WebSocketsProxyMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/WebSockets/WebSocketsProxyMiddlewareTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.WebSockets; -public class WebSocketsProxyMiddlewareTests +public class WebSocketsProxyMiddlewareTests : UnitTest { private readonly WebSocketsProxyMiddleware _middleware;