From 1748649bbe6221fddd289446b56d5a9db8d43e2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=99=E5=B0=8F=E7=AB=A0?= Date: Wed, 30 Oct 2024 19:16:19 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E5=B0=88=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ORM/EFCore/Lab.Sharding/.dockerignore | 11 + ORM/EFCore/Lab.Sharding/.editorconfig | 323 ++++++++++++++++ ORM/EFCore/Lab.Sharding/.gitignore | 347 +++++++++++++++++ ORM/EFCore/Lab.Sharding/Taskfile.yml | 65 ++++ ORM/EFCore/Lab.Sharding/docker-compose.yml | 31 ++ ORM/EFCore/Lab.Sharding/env/local.env | 5 + .../AutoGenerated/JobClient.cs | 193 ++++++++++ .../Lab.Sharding.Contract.csproj | 18 + .../AutoGenerated/Entities/Member.cs | 22 ++ .../AutoGenerated/Member1DbContext.cs | 39 ++ .../AutoGenerated/Member2DbContext.cs | 39 ++ .../AutoGenerated/MemberDbContext.cs | 39 ++ .../ConnectionStringProvider.cs | 30 ++ .../IDynamicDbContextFactory.cs | 29 ++ .../Lab.Sharding.DB/Lab.Sharding.DB.csproj | 15 + .../DateTimeExtensions.cs | 38 ++ .../DateTimeOffsetJsonConverter.cs | 27 ++ .../EnvironmentUtility.cs | 68 ++++ .../EnvironmentVariableBase.cs | 37 ++ .../IUuidProvider.cs | 11 + .../JsonSerializeFactory.cs | 32 ++ .../Lab.Sharding.Infrastructure.csproj | 13 + .../TraceContext/ContextAccessor.cs | 23 ++ .../TraceContext/ContextHolder.cs | 6 + .../TraceContext/IContextGetter.cs | 6 + .../TraceContext/IContextSetter.cs | 6 + .../Lab.Sharding.IntegrationTest/BaseStep.cs | 354 ++++++++++++++++++ .../DB/Scripts/NewFile1.txt | 0 .../DbContextExtensions.cs | 60 +++ .../Lab.Sharding.IntegrationTest.csproj | 50 +++ .../ScenarioContextExtension.cs | 144 +++++++ .../ServiceCollectionExtension.cs | 26 ++ .../TestAssistant.cs | 40 ++ .../TestServer.cs | 27 ++ .../Lab.Sharding.IntegrationTest/Usings.cs | 1 + .../\351\243\257\347\262\222.feature" | 176 +++++++++ .../_01_Demo/\351\243\257\347\262\222Step.cs" | 21 ++ .../Lab.Sharding.Test.csproj | 27 ++ .../src/Lab.Sharding.Test/UnitTest1.cs | 19 + .../Lab.Sharding.Testing.Common.csproj | 16 + .../MockServer/Contracts/Cookies.cs | 6 + .../MockServer/Contracts/HttpRequest.cs | 12 + .../MockServer/Contracts/HttpResponse.cs | 8 + .../Contracts/PutNewEndPointRequest.cs | 7 + .../MockServer/MockServerHelper.cs | 68 ++++ .../MockServer/MockedServerAssistant.cs | 45 +++ .../NpgsqlGenerateScript.cs | 58 +++ .../RedirectConsole.cs | 22 ++ .../SqlServerGenerateScript.cs | 32 ++ .../TestContainerFactory.cs | 66 ++++ .../src/Lab.Sharding.WebAPI/CacheKeys.cs | 6 + .../AutoGenerated/ContractControllers.cs | 213 +++++++++++ .../CursorPaginatedList.cs | 17 + .../EnvironmentVariables.cs | 13 + .../src/Lab.Sharding.WebAPI/Failure.cs | 44 +++ .../src/Lab.Sharding.WebAPI/FailureCode.cs | 8 + .../Lab.Sharding.WebAPI.csproj | 33 ++ .../Member/GetMemberResponse.cs | 14 + .../Member/InsertMemberRequest.cs | 10 + .../src/Lab.Sharding.WebAPI/Member/Member.cs | 20 + .../Lab.Sharding.WebAPI/Member/MemberChain.cs | 66 ++++ .../Member/MemberController.cs | 81 ++++ .../Member/MemberHandler.cs | 125 +++++++ .../Member/MemberRepository.cs | 207 ++++++++++ .../src/Lab.Sharding.WebAPI/PaginatedList.cs | 33 ++ .../src/Lab.Sharding.WebAPI/Program.cs | 158 ++++++++ .../ServiceCollectionExtension.cs | 61 +++ .../src/Lab.Sharding.WebAPI/SysHeaderNames.cs | 6 + .../src/Lab.Sharding.WebAPI/TraceContext.cs | 14 + .../TraceContextMiddleware.cs | 77 ++++ .../appsettings.Development.json | 8 + .../src/Lab.Sharding.WebAPI/appsettings.json | 9 + ORM/EFCore/Lab.Sharding/src/Lab.Sharding.sln | 64 ++++ 73 files changed, 4045 insertions(+) create mode 100644 ORM/EFCore/Lab.Sharding/.dockerignore create mode 100644 ORM/EFCore/Lab.Sharding/.editorconfig create mode 100644 ORM/EFCore/Lab.Sharding/.gitignore create mode 100644 ORM/EFCore/Lab.Sharding/Taskfile.yml create mode 100644 ORM/EFCore/Lab.Sharding/docker-compose.yml create mode 100644 ORM/EFCore/Lab.Sharding/env/local.env create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Contract/AutoGenerated/JobClient.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Contract/Lab.Sharding.Contract.csproj create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/Entities/Member.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/Member1DbContext.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/Member2DbContext.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/MemberDbContext.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/ConnectionStringProvider.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/IDynamicDbContextFactory.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/Lab.Sharding.DB.csproj create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/DateTimeExtensions.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/DateTimeOffsetJsonConverter.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/EnvironmentUtility.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/EnvironmentVariableBase.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/IUuidProvider.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/JsonSerializeFactory.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/Lab.Sharding.Infrastructure.csproj create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/ContextAccessor.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/ContextHolder.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/IContextGetter.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/IContextSetter.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/BaseStep.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/DB/Scripts/NewFile1.txt create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/DbContextExtensions.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/Lab.Sharding.IntegrationTest.csproj create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/ScenarioContextExtension.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/ServiceCollectionExtension.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/TestAssistant.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/TestServer.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/Usings.cs create mode 100644 "ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/_01_Demo/\351\243\257\347\262\222.feature" create mode 100644 "ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/_01_Demo/\351\243\257\347\262\222Step.cs" create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Test/Lab.Sharding.Test.csproj create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Test/UnitTest1.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/Lab.Sharding.Testing.Common.csproj create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/Cookies.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/HttpRequest.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/HttpResponse.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/PutNewEndPointRequest.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/MockServerHelper.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/MockedServerAssistant.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/NpgsqlGenerateScript.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/RedirectConsole.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/SqlServerGenerateScript.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/TestContainerFactory.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/CacheKeys.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Contract/AutoGenerated/ContractControllers.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/CursorPaginatedList.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/EnvironmentVariables.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Failure.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/FailureCode.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Lab.Sharding.WebAPI.csproj create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/GetMemberResponse.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/InsertMemberRequest.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/Member.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberChain.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberController.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberHandler.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberRepository.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/PaginatedList.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Program.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/ServiceCollectionExtension.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/SysHeaderNames.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/TraceContext.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/TraceContextMiddleware.cs create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/appsettings.Development.json create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/appsettings.json create mode 100644 ORM/EFCore/Lab.Sharding/src/Lab.Sharding.sln diff --git a/ORM/EFCore/Lab.Sharding/.dockerignore b/ORM/EFCore/Lab.Sharding/.dockerignore new file mode 100644 index 00000000..53932437 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/.dockerignore @@ -0,0 +1,11 @@ +k8s +README.md +.gitignore +.vscode +.gitlab-ci.yml +ci.custom.script.sh + +**/bin/ +**/obj/ + +node_modules diff --git a/ORM/EFCore/Lab.Sharding/.editorconfig b/ORM/EFCore/Lab.Sharding/.editorconfig new file mode 100644 index 00000000..3ca9e23d --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/.editorconfig @@ -0,0 +1,323 @@ +## C# formatting options: https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/csharp-formatting-options + +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +#### Core EditorConfig Options #### +[*] + +# Indentation and spacing +end_of_line = lf +indent_size = 4 +indent_style = tab +tab_width = 4 +charset = utf-8 + + +# C# files +[*.cs] + +### C# formatting options +## New-line options +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +## Indentation options +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents_when_block = false + +## Spacing options +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_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_call_parameter_list_parentheses = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_after_comma = true +csharp_space_before_comma = false +csharp_space_after_dot = false +csharp_space_before_dot = false +csharp_space_after_semicolon_in_for_statement = true +csharp_space_before_semicolon_in_for_statement = false +csharp_space_around_declaration_statements = false +csharp_space_before_open_square_brackets = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_square_brackets = false + +## Wrap options +# Reference: https://learn.microsoft.com/zh-tw/dotnet/fundamentals/code-analysis/style-rules/ide0011 +csharp_prefer_braces = true:silent +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true + +### .NET formatting options + +## Using directive options +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = true + +## Dotnet namespace options +dotnet_style_namespace_match_folder = true + + +#### .NET Coding Conventions #### + +# this. and Me. preferences +dotnet_style_qualification_for_method = true + +# var suggestion options +csharp_style_var_elsewhere = true:suggestion +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion + +# naming style options +dotnet_naming_rule.constants_rule.severity = warning +dotnet_naming_rule.constants_rule.style = all_upper_style +dotnet_naming_rule.constants_rule.symbols = constants_symbols +dotnet_naming_rule.private_constants_rule.severity = warning +dotnet_naming_rule.private_constants_rule.style = lower_camel_case_style +dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols +dotnet_naming_rule.private_static_fields_rule.severity = warning +dotnet_naming_rule.private_static_fields_rule.style = s_lower_camel_case_style +dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols +dotnet_naming_rule.private_static_readonly_rule.severity = warning +dotnet_naming_rule.private_static_readonly_rule.style = s_lower_camel_case_style +dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols +dotnet_naming_rule.public_fields_rule.severity = warning +dotnet_naming_rule.public_fields_rule.style = upper_camel_case_style +dotnet_naming_rule.public_fields_rule.symbols = public_fields_symbols +dotnet_naming_rule.static_readonly_rule.severity = warning +dotnet_naming_rule.static_readonly_rule.style = all_upper_style +dotnet_naming_rule.static_readonly_rule.symbols = static_readonly_symbols +dotnet_naming_rule.type_parameters_rule.severity = warning +dotnet_naming_rule.type_parameters_rule.style = upper_camel_case_style +dotnet_naming_rule.type_parameters_rule.symbols = type_parameters_symbols +dotnet_naming_rule.unity_serialized_field_rule.severity = warning +dotnet_naming_rule.unity_serialized_field_rule.style = lower_camel_case_style +dotnet_naming_rule.unity_serialized_field_rule.symbols = unity_serialized_field_symbols +dotnet_naming_style.all_upper_style.capitalization = all_upper +dotnet_naming_style.all_upper_style.word_separator = _ +dotnet_naming_style.lower_camel_case_style.capitalization = camel_case +dotnet_naming_style.s_lower_camel_case_style.capitalization = camel_case +dotnet_naming_style.s_lower_camel_case_style.required_prefix = s_ +dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case +dotnet_naming_symbols.constants_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected +dotnet_naming_symbols.constants_symbols.applicable_kinds = field +dotnet_naming_symbols.constants_symbols.required_modifiers = const +dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field +dotnet_naming_symbols.private_constants_symbols.required_modifiers = const +dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static +dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static, readonly +dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected +dotnet_naming_symbols.public_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.static_readonly_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected +dotnet_naming_symbols.static_readonly_symbols.applicable_kinds = field +dotnet_naming_symbols.static_readonly_symbols.required_modifiers = static, readonly +dotnet_naming_symbols.type_parameters_symbols.applicable_accessibilities = * +dotnet_naming_symbols.type_parameters_symbols.applicable_kinds = type_parameter +dotnet_naming_symbols.unity_serialized_field_symbols.applicable_accessibilities = * +dotnet_naming_symbols.unity_serialized_field_symbols.applicable_kinds = +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_qualification_for_event = true:suggestion +dotnet_style_qualification_for_field = true:suggestion +dotnet_style_qualification_for_method = true:suggestion +dotnet_style_qualification_for_property = true:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion + + +# Sort using and Import directives with System.* appearing first +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false + +# Avoid "this." and "Me." if not necessary +dotnet_style_qualification_for_field = true +dotnet_style_qualification_for_property = true +dotnet_style_qualification_for_method = true +dotnet_style_qualification_for_event = true + +# Use language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# Suggest more modern language features when available +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion + +# Whitespace options +dotnet_style_allow_multiple_blank_lines_experimental = false + +# Non-private static fields are PascalCase +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style + +dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_private_static_fields.required_modifiers = static + +dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case + +# Non-private readonly fields are PascalCase +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_field_style + +dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly + +dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case + +# Constants are PascalCase + +dotnet_naming_symbols.constants.applicable_kinds = field, local +dotnet_naming_symbols.constants.required_modifiers = const + +dotnet_naming_style.constant_style.capitalization = pascal_case + +# Static fields are camelCase and start with s_ +dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion +dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields +dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style + +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static + +dotnet_naming_style.static_field_style.capitalization = camel_case +dotnet_naming_style.static_field_style.required_prefix = s_ + +# Instance fields are camelCase and start with _ +dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion +dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields +dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style + +dotnet_naming_symbols.instance_fields.applicable_kinds = field + +dotnet_naming_style.instance_field_style.capitalization = camel_case +dotnet_naming_style.instance_field_style.required_prefix = _ + +# Locals and parameters are camelCase +dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion +dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters +dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style + +dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local + +dotnet_naming_style.camel_case_style.capitalization = camel_case + +# Local functions are PascalCase +dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions +dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style + +dotnet_naming_symbols.local_functions.applicable_kinds = local_function + +dotnet_naming_style.local_function_style.capitalization = pascal_case + +# By default, name items with PascalCase +dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members +dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style + +dotnet_naming_symbols.all_members.applicable_kinds = * + +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# RS0016: Only enable if API files are present +dotnet_public_api_analyzer.require_api_files = true + +#### Diagnostic configuration #### + +# CA1000: Do not declare static members on generic types +dotnet_diagnostic.CA1000.severity = warning + +# IDE0055: Fix formatting +# Reference: https://learn.microsoft.com/zh-tw/dotnet/fundamentals/code-analysis/style-rules/ide0055 +dotnet_diagnostic.IDE0055.severity = warning + +# error RS2008: Enable analyzer release tracking for the analyzer project containing rule '{0}' +dotnet_diagnostic.rs2008.severity = none + +# IDE0035: Remove unreachable code +dotnet_diagnostic.ide0035.severity = warning + +# IDE0036: Order modifiers +dotnet_diagnostic.ide0036.severity = warning + +# IDE0043: Format string contains invalid placeholder +dotnet_diagnostic.ide0043.severity = warning + +# IDE0044: Make field readonly +dotnet_diagnostic.ide0044.severity = warning + +# dotnet_style_allow_multiple_blank_lines_experimental +dotnet_diagnostic.ide2000.severity = warning + +# csharp_style_allow_embedded_statements_on_same_line_experimental +dotnet_diagnostic.ide2001.severity = warning + +# csharp_style_allow_blank_lines_between_consecutive_braces_experimental +dotnet_diagnostic.ide2002.severity = warning + +# dotnet_style_allow_statement_immediately_after_block_experimental +dotnet_diagnostic.ide2003.severity = warning + +# csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental +dotnet_diagnostic.ide2004.severity = warning + +# IDE0011: Add braces +csharp_prefer_braces = when_multiline:warning + +# NOTE: We need the below severity entry for Add Braces due to https://github.com/dotnet/roslyn/issues/44201 +dotnet_diagnostic.ide0011.severity = warning + +# IDE0040: Add accessibility modifiers +dotnet_diagnostic.ide0040.severity = warning + +# CONSIDER: Are IDE0051 and IDE0052 too noisy to be warnings for IDE editing scenarios? Should they be made build-only warnings? +# IDE0051: Remove unused private member +dotnet_diagnostic.ide0051.severity = warning + +# IDE0052: Remove unread private member +dotnet_diagnostic.ide0052.severity = warning + +# IDE0059: Unnecessary assignment to a value +dotnet_diagnostic.ide0059.severity = warning + +# IDE0060: Remove unused parameter +dotnet_diagnostic.ide0060.severity = warning + +# CA1012: Abstract types should not have public constructors +dotnet_diagnostic.ca1012.severity = warning + +# CA1822: Make member static +dotnet_diagnostic.ca1822.severity = warning + +[{*.yaml,*.yml}] +indent_size = 2 +tab_width = 2 \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/.gitignore b/ORM/EFCore/Lab.Sharding/.gitignore new file mode 100644 index 00000000..e6796067 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/.gitignore @@ -0,0 +1,347 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +#vistual studio extension +.localhistory + +# stress test output +stress/output + +# specflow feature.cs +**/*.feature.cs + +# secrets +secret/ +logs/ + +.DS_Store +*.zip + diff --git a/ORM/EFCore/Lab.Sharding/Taskfile.yml b/ORM/EFCore/Lab.Sharding/Taskfile.yml new file mode 100644 index 00000000..2b747cfe --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/Taskfile.yml @@ -0,0 +1,65 @@ +# Taskfile.yml + +version: "3" + +dotenv: [ "env/local.env" ] + +tasks: + ## Develop --------------------------------------------------- + dev-init: + desc: Init development environment + cmds: + - task: redis-start + + redis-start: + desc: start redis 5.X version + cmds: + - docker-compose up -d redis + + redis-admin-start: + desc: admin ui to manage redis + cmds: + - docker-compose up -d redis-admin + + ef-codegen: + desc: Init development environment + cmds: + - task: ef-codegen-member + + ef-codegen-member: + desc: EF Core 反向工程產生 MemberDbContext EF Entities + dir: "src/be/Lab.Sharding.DB" + cmds: + - dotnet ef dbcontext scaffold "$SYS_DATABASE_CONNECTION_STRING" Microsoft.EntityFrameworkCore.SqlServer -o AutoGenerated/Entities -c MemberDbContext --context-dir AutoGenerated/ -n Lab.Sharding.DB -t Member --force --no-onconfiguring --use-database-names + - dotnet ef dbcontext scaffold "$SYS_DATABASE_CONNECTION_STRING" Microsoft.EntityFrameworkCore.SqlServer -o AutoGenerated/Entities -c Member1DbContext --context-dir AutoGenerated/ -n Lab.Sharding.DB -t Member --force --no-onconfiguring --use-database-names + + codegen-api: + desc: codegen client and server + cmds: + - task: codegen-api-client + - task: codegen-api-server + codegen-api-client: + desc: codegen client + cmds: + - refitter ./doc/openapi.yml --namespace "Lab.Sharding.Contract" --output ./src/be/Lab.Sharding.Contract/AutoGenerated/JobClient.cs --use-api-response --no-operation-headers --no-auto-generated-header + codegen-api-server: + desc: codegen server + cmds: + - nswag openapi2cscontroller /input:./doc/openapi.yml /classname:{controller} /namespace:Lab.Sharding.WebAPI.Contract /output:./src/be/Lab.Sharding.WebAPI/Contract/AutoGenerated/ContractControllers.cs /jsonLibrary:SystemTextJson /useCancellationToken:true /useActionResultType:true /operationGenerationMode:MultipleClientsFromFirstTagAndOperationId /controllerBaseClass:Microsoft.AspNetCore.Mvc.ControllerBase /excludedParameterNames:x-idempotency-key,x-api-key + + codegen-api-doc: + desc: codegen api doc + 安裝 Redocly CLI + npm install -g @redocly/openapi-cli + dir: "./doc" + cmds: + - redocly build-docs ./openapi.yml --output ./openapi.html +# - redocly build-docs ./doc/openapi.yml --output ./doc/openapi.html +# - redocly preview-docs ./doc/openapi.yml + #- redocly bundle ./doc/openapi.yml --output ./doc/openapi-bundled.yml + #- redocly preview-docs ./doc/openapi-bundled.yml + codegen-api-preview: + desc: codegen api doc + dir: "./doc" + cmds: + - redocly preview-docs ./openapi.yml diff --git a/ORM/EFCore/Lab.Sharding/docker-compose.yml b/ORM/EFCore/Lab.Sharding/docker-compose.yml new file mode 100644 index 00000000..1826da2b --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/docker-compose.yml @@ -0,0 +1,31 @@ +services: + seq: + image: datalust/seq:latest + ports: + - "5341:5341" + environment: + - ACCEPT_EULA=Y + redis: + image: redis:5.0.12-alpine + #command: [ "redis-server", "--bind", "redis", "--port", "6379" ] + ports: + - 6379:6379 + + # 連不上的話 host 打 redis:6379 + redis-admin: + image: marian/rebrow + ports: + - 8006:5001 + depends_on: + - redis + + sql2019: + image: 'mcr.microsoft.com/mssql/server:2019-latest' + hostname: 'sql2019' + container_name: 'sql2019' + ports: + - 1433:1433 + environment: + - ACCEPT_EULA=Y + - MSSQL_SA_PASSWORD=pass@w0rd1~ + - MSSQL_PID=Express diff --git a/ORM/EFCore/Lab.Sharding/env/local.env b/ORM/EFCore/Lab.Sharding/env/local.env new file mode 100644 index 00000000..49a50eca --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/env/local.env @@ -0,0 +1,5 @@ +#sql server connection string +SYS_DATABASE_CONNECTION_STRING=Server=localhost;Database=Member01;User Id=SA;Password=pass@w0rd1~;TrustServerCertificate=True +SYS_DATABASE_CONNECTION_STRING=Server=localhost;Database=Member01;User Id=SA;Password=pass@w0rd1~;TrustServerCertificate=True +SYS_REDIS_URL=localhost:6379 +EXTERNAL_API=http://localhost:5000/api diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Contract/AutoGenerated/JobClient.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Contract/AutoGenerated/JobClient.cs new file mode 100644 index 00000000..982dfff5 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Contract/AutoGenerated/JobClient.cs @@ -0,0 +1,193 @@ +using System.Text.Json.Serialization; +using Refit; + +namespace Lab.Sharding.Contract.AutoGenerated; +#nullable enable annotations + +[System.CodeDom.Compiler.GeneratedCode("Refitter", "1.4.0.0")] +public partial interface IJobBank1111JobWebAPI +{ + /// + /// A representing the instance containing the result: + /// + /// + /// Status + /// Description + /// + /// + /// 200 + /// OK + /// + /// + /// 400 + /// Bad Request + /// + /// + /// + [Headers("Accept: text/plain, application/json, text/json")] + [Get("/api/v1/tagscursor")] + Task> GetTagsCursor(); + + /// + /// A representing the instance containing the result: + /// + /// + /// Status + /// Description + /// + /// + /// 200 + /// OK + /// + /// + /// + [Headers("Accept: text/plain, application/json, text/json")] + [Get("/api/v1/memberscursor")] + Task> GetMembersCursor(); + + /// + /// A representing the instance containing the result: + /// + /// + /// Status + /// Description + /// + /// + /// 200 + /// OK + /// + /// + /// + [Headers("Accept: text/plain, application/json, text/json")] + [Get("/api/v1/membersoffset")] + Task> GetMemberOffset(); + + /// + /// A representing the instance containing the result: + /// + /// + /// Status + /// Description + /// + /// + /// 200 + /// OK + /// + /// + /// + [Post("/api/v2/members")] + Task InsertMember2([Body] InsertMemberRequest body); + + /// + /// A representing the instance containing the result: + /// + /// + /// Status + /// Description + /// + /// + /// 200 + /// OK + /// + /// + /// + [Post("/api/v1/members")] + Task InsertMember1([Body] InsertMemberRequest body); + + +} + +//---------------------- +// +// Generated using the NSwag toolchain v14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org) +// +//---------------------- + +#pragma warning disable 108 // Disable "CS0108 '{derivedDto}.ToJson()' hides inherited member '{dtoBase}.ToJson()'. Use the new keyword if hiding was intended." +#pragma warning disable 114 // Disable "CS0114 '{derivedDto}.RaisePropertyChanged(String)' hides inherited member 'dtoBase.RaisePropertyChanged(String)'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword." +#pragma warning disable 472 // Disable "CS0472 The result of the expression is always 'false' since a value of type 'Int32' is never equal to 'null' of type 'Int32?' +#pragma warning disable 612 // Disable "CS0612 '...' is obsolete" +#pragma warning disable 649 // Disable "CS0649 Field is never assigned to, and will always have its default value null" +#pragma warning disable 1573 // Disable "CS1573 Parameter '...' has no matching param tag in the XML comment for ... +#pragma warning disable 1591 // Disable "CS1591 Missing XML comment for publicly visible type or member ..." +#pragma warning disable 8073 // Disable "CS8073 The result of the expression is always 'false' since a value of type 'T' is never equal to 'null' of type 'T?'" +#pragma warning disable 3016 // Disable "CS3016 Arrays as attribute arguments is not CLS-compliant" +#pragma warning disable 8603 // Disable "CS8603 Possible null reference return" +#pragma warning disable 8604 // Disable "CS8604 Possible null reference argument for parameter" +#pragma warning disable 8625 // Disable "CS8625 Cannot convert null literal to non-nullable reference type" +#pragma warning disable 8765 // Disable "CS8765 Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes)." + +[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] +public partial class GetMemberResponse +{ + + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("age")] + public int? Age { get; set; } + + [JsonPropertyName("email")] + public string Email { get; set; } + + [JsonPropertyName("sequenceId")] + public long? SequenceId { get; set; } + +} + +[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] +public partial class GetMemberResponseCursorPaginatedList +{ + + [JsonPropertyName("items")] + public ICollection Items { get; set; } + + [JsonPropertyName("nextPageToken")] + public string NextPageToken { get; set; } + + [JsonPropertyName("nextPreviousToken")] + public string NextPreviousToken { get; set; } + +} + +[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] +public partial class InsertMemberRequest +{ + + [JsonPropertyName("email")] + public string Email { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("age")] + public int Age { get; set; } + +} + +[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] +public partial class Failure +{ + + [JsonPropertyName("code")] + public string Code { get; set; } + + [JsonPropertyName("reason")] + public string Reason { get; set; } + +} + +#pragma warning restore 108 +#pragma warning restore 114 +#pragma warning restore 472 +#pragma warning restore 612 +#pragma warning restore 1573 +#pragma warning restore 1591 +#pragma warning restore 8073 +#pragma warning restore 3016 +#pragma warning restore 8603 +#pragma warning restore 8604 +#pragma warning restore 8625 \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Contract/Lab.Sharding.Contract.csproj b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Contract/Lab.Sharding.Contract.csproj new file mode 100644 index 00000000..6b1fec70 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Contract/Lab.Sharding.Contract.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/Entities/Member.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/Entities/Member.cs new file mode 100644 index 00000000..21ed1d44 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/Entities/Member.cs @@ -0,0 +1,22 @@ +namespace Lab.Sharding.DB.AutoGenerated.Entities; + +public partial class Member +{ + public string Id { get; set; } = null!; + + public string? Name { get; set; } + + public int? Age { get; set; } + + public long SequenceId { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public string? CreatedBy { get; set; } + + public DateTimeOffset? ChangedAt { get; set; } + + public string? ChangedBy { get; set; } + + public string? Email { get; set; } +} diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/Member1DbContext.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/Member1DbContext.cs new file mode 100644 index 00000000..f23f7ce8 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/Member1DbContext.cs @@ -0,0 +1,39 @@ +using Lab.Sharding.DB.AutoGenerated.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Lab.Sharding.DB.AutoGenerated; + +public partial class Member1DbContext : DbContext +{ + public Member1DbContext(DbContextOptions options) + : base(options) + { + } + + public virtual DbSet Members { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("Member_pk"); + + entity.ToTable("Member"); + + entity.Property(e => e.Id) + .HasMaxLength(50) + .IsUnicode(false); + entity.Property(e => e.ChangedBy).HasMaxLength(20); + entity.Property(e => e.CreatedBy).HasMaxLength(50); + entity.Property(e => e.Email) + .HasMaxLength(50) + .IsUnicode(false); + entity.Property(e => e.Name).HasMaxLength(20); + entity.Property(e => e.SequenceId).ValueGeneratedOnAdd(); + }); + + this.OnModelCreatingPartial(modelBuilder); + } + + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); +} diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/Member2DbContext.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/Member2DbContext.cs new file mode 100644 index 00000000..091b6d55 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/Member2DbContext.cs @@ -0,0 +1,39 @@ +using Lab.Sharding.DB.AutoGenerated.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Lab.Sharding.DB.AutoGenerated; + +public partial class Member2DbContext : DbContext +{ + public Member2DbContext(DbContextOptions options) + : base(options) + { + } + + public virtual DbSet Members { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("Member_pk"); + + entity.ToTable("Member"); + + entity.Property(e => e.Id) + .HasMaxLength(50) + .IsUnicode(false); + entity.Property(e => e.ChangedBy).HasMaxLength(20); + entity.Property(e => e.CreatedBy).HasMaxLength(50); + entity.Property(e => e.Email) + .HasMaxLength(50) + .IsUnicode(false); + entity.Property(e => e.Name).HasMaxLength(20); + entity.Property(e => e.SequenceId).ValueGeneratedOnAdd(); + }); + + this.OnModelCreatingPartial(modelBuilder); + } + + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); +} diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/MemberDbContext.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/MemberDbContext.cs new file mode 100644 index 00000000..4b7faba6 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/MemberDbContext.cs @@ -0,0 +1,39 @@ +using Lab.Sharding.DB.AutoGenerated.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Lab.Sharding.DB.AutoGenerated; + +public partial class MemberDbContext : DbContext +{ + public MemberDbContext(DbContextOptions options) + : base(options) + { + } + + public virtual DbSet Members { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("Member_pk"); + + entity.ToTable("Member"); + + entity.Property(e => e.Id) + .HasMaxLength(50) + .IsUnicode(false); + entity.Property(e => e.ChangedBy).HasMaxLength(20); + entity.Property(e => e.CreatedBy).HasMaxLength(50); + entity.Property(e => e.Email) + .HasMaxLength(50) + .IsUnicode(false); + entity.Property(e => e.Name).HasMaxLength(20); + entity.Property(e => e.SequenceId).ValueGeneratedOnAdd(); + }); + + this.OnModelCreatingPartial(modelBuilder); + } + + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); +} diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/ConnectionStringProvider.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/ConnectionStringProvider.cs new file mode 100644 index 00000000..e79dc73a --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/ConnectionStringProvider.cs @@ -0,0 +1,30 @@ +namespace Lab.Sharding.DB; + +public interface IConnectionStringProvider +{ + string GetConnectionString(string databaseIdentifier); + + void SetConnectionStrings(Dictionary connectionStrings); +} + +public class ConnectionStringProvider : IConnectionStringProvider +{ + private Dictionary _connectionStrings; + + public ConnectionStringProvider() + { + + } + + public void SetConnectionStrings(Dictionary connectionStrings) + { + this._connectionStrings = connectionStrings; + } + + public string GetConnectionString(string databaseIdentifier) + { + return this._connectionStrings.TryGetValue(databaseIdentifier, out var connectionString) + ? connectionString + : throw new ArgumentException("Unknown database identifier"); + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/IDynamicDbContextFactory.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/IDynamicDbContextFactory.cs new file mode 100644 index 00000000..57b8dcc0 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/IDynamicDbContextFactory.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore; + +namespace Lab.Sharding.DB; + +public interface IDynamicDbContextFactory + where TContext : DbContext +{ + TContext CreateDbContext(string connectionString); +} + +public class DynamicDbContextFactory : IDynamicDbContextFactory + where TContext : DbContext +{ + private readonly DbContextOptionsBuilder _optionsBuilder; + private readonly IConnectionStringProvider _connectionStringProvider; + + public DynamicDbContextFactory(IConnectionStringProvider connectionStringProvider) + { + this._connectionStringProvider = connectionStringProvider; + this._optionsBuilder = new DbContextOptionsBuilder(); + } + + public TContext CreateDbContext(string databaseIdentifier) + { + var connectionString = this._connectionStringProvider.GetConnectionString(databaseIdentifier); + this._optionsBuilder.UseSqlServer(connectionString); + return (TContext)Activator.CreateInstance(typeof(TContext), _optionsBuilder.Options); + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/Lab.Sharding.DB.csproj b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/Lab.Sharding.DB.csproj new file mode 100644 index 00000000..4a0e2409 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/Lab.Sharding.DB.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + + + + + + + + + diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/DateTimeExtensions.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/DateTimeExtensions.cs new file mode 100644 index 00000000..aadd6a01 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/DateTimeExtensions.cs @@ -0,0 +1,38 @@ +using System.Globalization; + +namespace Lab.Sharding.Infrastructure; + +public static class DateTimeExtensions +{ + public const string DefaultDateTimeFormat = "o"; + public static readonly CultureInfo DefaultDateTimeCultureInfo = CultureInfo.InvariantCulture; + + public static IEnumerable<(DateTime start, DateTime end)> Each(this DateTime inputStart, DateTime inputEnd, + TimeSpan step) + { + DateTime dtStart, dtEnd; + dtStart = inputStart; + while (dtStart < inputEnd) + { + dtEnd = dtStart + step; + if (dtEnd > inputEnd) + { + dtEnd = inputEnd; + } + + yield return (dtStart, dtEnd); + + dtStart += step; + } + } + + public static string ToUtcString(this DateTimeOffset dto) + { + return dto.UtcDateTime.ToString(DefaultDateTimeFormat, DefaultDateTimeCultureInfo); + } + + public static string ToUtcString(this DateTime dt) + { + return dt.ToString(DefaultDateTimeFormat, DefaultDateTimeCultureInfo); + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/DateTimeOffsetJsonConverter.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/DateTimeOffsetJsonConverter.cs new file mode 100644 index 00000000..57f19363 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/DateTimeOffsetJsonConverter.cs @@ -0,0 +1,27 @@ +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Lab.Sharding.Infrastructure; + +public class DateTimeOffsetJsonConverter : JsonConverter +{ + public override DateTimeOffset Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) + { + return DateTimeOffset.Parse( + reader.GetString(), + DateTimeExtensions.DefaultDateTimeCultureInfo, + DateTimeStyles.AdjustToUniversal); + } + + public override void Write( + Utf8JsonWriter writer, + DateTimeOffset dateTimeValue, + JsonSerializerOptions options) + { + writer.WriteStringValue(dateTimeValue.ToUtcString()); + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/EnvironmentUtility.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/EnvironmentUtility.cs new file mode 100644 index 00000000..c51312f3 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/EnvironmentUtility.cs @@ -0,0 +1,68 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Lab.Sharding.Infrastructure +{ + public static class EnvironmentUtility + { + public static string FindParentFolder(string parentFolderName) + { + var currentDirectory = AppDomain.CurrentDomain.BaseDirectory; + var directory = new DirectoryInfo(currentDirectory); + + while (directory != null) + { + var folders = Directory.GetDirectories(directory.FullName, parentFolderName); + if (folders.Length > 0) + { + return folders[0]; + } + + directory = directory.Parent; + } + + throw new DirectoryNotFoundException($"Folder '{parentFolderName}' not found."); + } + + public static void ReadEnvironmentFile(string folder, string fileName) + { + var fileFullPath = Path.Combine(folder, fileName); + if (!File.Exists(fileFullPath)) + { + throw new FileNotFoundException($"File '{fileName}' not found."); + } + + //讀取 .env 檔案 + var lines = File.ReadAllLines(fileFullPath); + foreach (var line in lines) + { + //排除註解 + if (line.StartsWith("#")) + { + continue; + } + + var parts = line.Split('='); + if (parts.Length < 2) + { + continue; + } + + var key = parts[0]; + var value = ""; + for (int i = 1; i < parts.Length; i++) + { + if (i == 1) + { + value += parts[i]; + continue; + } + + value += $"={parts[i]}"; + } + + Environment.SetEnvironmentVariable(key, value); + } + } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/EnvironmentVariableBase.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/EnvironmentVariableBase.cs new file mode 100644 index 00000000..05028b30 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/EnvironmentVariableBase.cs @@ -0,0 +1,37 @@ +namespace Lab.Sharding.Infrastructure; + +public record EnvironmentVariableBase +{ + public string KeyName { get; } + + public T Value { get; } + + public EnvironmentVariableBase(Func converter, bool isRequired = true) + { + this.KeyName = this.GetType().Name; + + string rawValue; + try + { + rawValue = Environment.GetEnvironmentVariable(this.KeyName); + } + catch + { + rawValue = null; + } + + if (isRequired && string.IsNullOrWhiteSpace(rawValue)) + { + throw new ArgumentNullException($"EnvironmentVariable({this.KeyName}) was required."); + } + + this.Value = converter(rawValue); + } +} + +public record EnvironmentVariableBase : EnvironmentVariableBase +{ + public EnvironmentVariableBase(bool isRequired = true) : base(x => x, isRequired) + { + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/IUuidProvider.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/IUuidProvider.cs new file mode 100644 index 00000000..3ba3562c --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/IUuidProvider.cs @@ -0,0 +1,11 @@ +namespace Lab.Sharding.Infrastructure; + +public interface IUuidProvider +{ + public string NewId(); +} + +public class UuidProvider : IUuidProvider +{ + public string NewId() => Guid.NewGuid().ToString(); +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/JsonSerializeFactory.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/JsonSerializeFactory.cs new file mode 100644 index 00000000..39cf2acd --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/JsonSerializeFactory.cs @@ -0,0 +1,32 @@ +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Lab.Sharding.Infrastructure; + +public class JsonSerializeFactory +{ + private static readonly Lazy s_defaultOptionLazy = new(CreateDefaultOptions); + + public static JsonSerializerOptions DefaultOptions => s_defaultOptionLazy.Value; + + public static JsonSerializerOptions CreateDefaultOptions() + { + var options = new JsonSerializerOptions(); + Apply(options); + return options; + } + + public static JsonSerializerOptions Apply(JsonSerializerOptions options) + { + options.MaxDepth = 20; + options.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + options.PropertyNameCaseInsensitive = true; + options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; + options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + options.Converters.Add(new JsonStringEnumConverter()); + options.Converters.Add(new DateTimeOffsetJsonConverter()); + return options; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/Lab.Sharding.Infrastructure.csproj b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/Lab.Sharding.Infrastructure.csproj new file mode 100644 index 00000000..b6e3fde8 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/Lab.Sharding.Infrastructure.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/ContextAccessor.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/ContextAccessor.cs new file mode 100644 index 00000000..2215f6d3 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/ContextAccessor.cs @@ -0,0 +1,23 @@ +namespace Lab.Sharding.Infrastructure.TraceContext; + +public class ContextAccessor : IContextSetter, IContextGetter + where T : class +{ + private static readonly AsyncLocal> s_current = new(); + + public T? Get() + { + var contextHolder = s_current.Value; + return contextHolder?.Value; + } + + public void Set(T value) + { + if (s_current.Value == null) + { + s_current.Value = new ContextHolder(); + } + + s_current.Value.Value = value; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/ContextHolder.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/ContextHolder.cs new file mode 100644 index 00000000..2124f5bb --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/ContextHolder.cs @@ -0,0 +1,6 @@ +namespace Lab.Sharding.Infrastructure.TraceContext; + +public class ContextHolder +{ + public T Value { get; set; } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/IContextGetter.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/IContextGetter.cs new file mode 100644 index 00000000..bbe85ded --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/IContextGetter.cs @@ -0,0 +1,6 @@ +namespace Lab.Sharding.Infrastructure.TraceContext; + +public interface IContextGetter +{ + T? Get(); +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/IContextSetter.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/IContextSetter.cs new file mode 100644 index 00000000..e8b419a7 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/IContextSetter.cs @@ -0,0 +1,6 @@ +namespace Lab.Sharding.Infrastructure.TraceContext; + +public interface IContextSetter +{ + void Set(T value); +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/BaseStep.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/BaseStep.cs new file mode 100644 index 00000000..238974b8 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/BaseStep.cs @@ -0,0 +1,354 @@ +using System.Net.Mime; +using System.Text; +using System.Text.Json.JsonDiffPatch; +using System.Text.Json.JsonDiffPatch.Xunit; +using System.Text.Json.Nodes; +using FluentAssertions; +using Flurl; +using Lab.Sharding.DB; +using Lab.Sharding.Testing.Common; +using Lab.Sharding.Testing.Common.MockServer; +using Json.Path; +using Lab.Sharding.DB.AutoGenerated; +using Lab.Sharding.DB.AutoGenerated.Entities; +using Lab.Sharding.WebAPI; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Reqnroll; +using Xunit; +using Xunit.Abstractions; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] + +namespace JobBank1111.Job.WebAPI.IntegrationTest; + +[Binding] +[CollectionDefinition("Lab.Sharding.IntegrationTest", DisableParallelization = true)] +public class BaseStep : Steps +{ + private readonly ITestOutputHelper _testOutputHelper; + private static HttpClient ExternalClient; + static IServiceProvider ServiceProvider; + + private const string StringEquals = "字串等於"; + private const string NumberEquals = "數值等於"; + private const string BoolEquals = "布林值等於"; + private const string JsonEquals = "Json等於"; + private const string DateTimeEquals = "時間等於"; + + private const string OperationTypes = StringEquals + + "|" + NumberEquals + + "|" + BoolEquals + + "|" + JsonEquals + + "|" + DateTimeEquals; + + public BaseStep(ITestOutputHelper testOutputHelper) + { + this._testOutputHelper = testOutputHelper; + } + + [BeforeTestRun] + public static async Task BeforeTestRun() + { + //建立容器 + await CreateContainersAsync(); + TestAssistant.SetEnvironmentVariables(); + + //建立當前測試步驟所需要的 DI Containers + ServiceProvider = CreateServiceProvider(); + + await InitialDatabase(ServiceProvider); + + async Task InitialDatabase(IServiceProvider serviceProvider) + { + var dbContextFactory = serviceProvider.GetService>(); + await using var dbContext = await dbContextFactory.CreateDbContextAsync(); + await dbContext.Initial(); + } + + async Task CreateContainersAsync() + { + var msSqlContainer = await TestContainerFactory.CreateMsSqlContainerAsync(); + var dbConnectionString = msSqlContainer.GetConnectionString(); + TestAssistant.SetDbConnectionEnvironmentVariable(dbConnectionString); + var redisContainer = await TestContainerFactory.CreateRedisContainerAsync(); + var redisDomainUrl = redisContainer.GetConnectionString(); + TestAssistant.SetRedisConnectionEnvironmentVariable(redisDomainUrl); + + var mockServerContainer = await TestContainerFactory.CreateMockServerContainerAsync(); + var externalUrl = TestContainerFactory.GetMockServerConnection(mockServerContainer); + TestAssistant.SetExternalConnectionEnvironmentVariable(externalUrl); + ExternalClient = new HttpClient() { BaseAddress = new Uri(externalUrl) }; + } + } + + [BeforeScenario] + public async Task BeforeScenario() + { + this.ClearData(ServiceProvider); + } + + private static IServiceProvider CreateServiceProvider() + { + var services = new ServiceCollection(); + services.AddSysEnvironments(); + services.AddLogging(builder => builder.AddConsole()); + services.AddDatabase(); + + var serviceProvider = services.BuildServiceProvider(); + return serviceProvider; + } + + private void ClearData(IServiceProvider serviceProvider) + { + var contextFactory = serviceProvider.GetRequiredService>(); + using var dbContext = contextFactory.CreateDbContext(); + dbContext.ClearAllData(); + } + + [Given(@"資料庫已存在 Member 資料")] + public async Task Given資料庫已存在Member資料(Table table) + { + var userId = this.ScenarioContext.GetUserId(); + var now = this.ScenarioContext.GetUtcNow().Value; + var toDb = table.CreateSet(p => new Member + { + Id = null, + Name = null, + Age = null, + SequenceId = 0, + CreatedAt = now, + CreatedBy = userId, + ChangedAt = now, + ChangedBy = userId, + Email = null + }); + await using var dbContext = await this.ScenarioContext.GetMemberDbContextFactory().CreateDbContextAsync(); + await dbContext.Members.AddRangeAsync(toDb); + await dbContext.SaveChangesAsync(); + } + + [Given(@"建立假端點,HttpMethod = ""(.*)"",URL = ""(.*)"",StatusCode = ""(.*)"",ResponseContent =")] + public async Task Given建立假端點HttpMethodUrlStatusCodeResponseContent( + string httpMethod, string url, int statusCode, string body) + { + var client = ExternalClient; + await MockedServerAssistant.PutNewEndPointAsync(client, httpMethod, url, statusCode, body); + } + + [When(@"調用端發送 ""(.*)"" 請求至 ""(.*)""")] + public async Task When調用端發送請求至(string methodName, string url) + { + var client = this.ScenarioContext.GetHttpClient(); + + var httpMethod = new HttpMethod(methodName); + var urlSegments = Url.ParsePathSegments(url); + var urlEncoded = Url.Combine(urlSegments.ToArray()); + urlEncoded = this.AppendQuery(urlEncoded); + using var httpRequest = new HttpRequestMessage(httpMethod, urlEncoded); + + var contentType = MediaTypeNames.Application.Json; + var headers = this.ScenarioContext.GetOrNewHeaders(); + foreach (var header in headers) + { + if (header.Key == "content-type") + { + contentType = header.Value.First(); + } + else + { + httpRequest.Headers.Add(header.Key, header.Value.ToArray()); + } + } + + var body = this.ScenarioContext.GetHttpRequestBody(); + if (string.IsNullOrWhiteSpace(body) is false) + { + httpRequest.Content = new StringContent(body, Encoding.UTF8, contentType); + } + + var httpResponse = await client.SendAsync(httpRequest); + var responseBody = await httpResponse.Content.ReadAsStringAsync(); + this.ScenarioContext.SetHttpResponse(httpResponse); + this.ScenarioContext.SetHttpResponseBody(responseBody); + this.ScenarioContext.SetHttpStatusCode(httpResponse.StatusCode); + if (string.IsNullOrWhiteSpace(responseBody) == false) + { + Console.WriteLine(responseBody); + var jsonNode = JsonNode.Parse(responseBody); + this.ScenarioContext.SetJsonNode(jsonNode); + var jsonObject = jsonNode.AsObject(); + + if (jsonObject.TryGetPropertyValue("nextPageToken", out var nextPageTokenNode)) + { + var nextPageToken = nextPageTokenNode.GetValue(); + this.ScenarioContext.SetNextPageToken(nextPageToken); + } + } + } + + private static void ContentShouldBe(JsonNode srcJsonNode, string selectPath, string operationType, string expected) + { + var destJsonNode = JsonPath.Parse(selectPath); + switch (operationType) + { + case StringEquals: + { + var actual = destJsonNode.Evaluate(srcJsonNode).Matches.FirstOrDefault()?.Value?.GetValue(); + var errorReason = + $"{nameof(operationType)}: [{operationType}], {nameof(selectPath)}: [{selectPath}], {nameof(expected)}: [{expected}], {nameof(actual)}: [{actual}]"; + (actual ?? string.Empty).Should().Be(expected, errorReason); + break; + } + case NumberEquals: + { + var actual = destJsonNode.Evaluate(srcJsonNode).Matches.FirstOrDefault()?.Value?.GetValue(); + var errorReason = + $"{nameof(operationType)}: [{operationType}], {nameof(selectPath)}: [{selectPath}], {nameof(expected)}: [{expected}], {nameof(actual)}: [{actual}]"; + actual.Should().Be(int.Parse(expected), errorReason); + break; + } + case BoolEquals: + { + var actual = destJsonNode.Evaluate(srcJsonNode).Matches.FirstOrDefault()?.Value?.GetValue(); + var errorReason = + $"{nameof(operationType)}: [{operationType}], {nameof(selectPath)}: [{selectPath}], {nameof(expected)}: [{expected}], {nameof(actual)}: [{actual}]"; + actual.Should().Be(bool.Parse(expected), errorReason); + break; + } + case DateTimeEquals: + { + var expect = DateTimeOffset.Parse(expected); + var actual = destJsonNode.Evaluate(srcJsonNode).Matches.FirstOrDefault() + ?.Value + ?.GetValue() + ; + var errorReason = + $"{nameof(operationType)}: [{operationType}], {nameof(selectPath)}: [{selectPath}], {nameof(expected)}: [{expect}], {nameof(actual)}: [{actual}]"; + actual.Should().Be(expect, errorReason); + break; + } + case JsonEquals: + { + var actual = destJsonNode.Evaluate(srcJsonNode).Matches.FirstOrDefault()?.Value; + var expect = string.IsNullOrWhiteSpace(expected) ? null : JsonNode.Parse(expected); + var diff = actual.Diff(expect); + var errorReason = + $"{nameof(operationType)}: [{operationType}], {nameof(selectPath)}: [{selectPath}], {nameof(expected)}: [{expected}], {nameof(actual)}: [{actual?.ToJsonString()}], diff: [{diff?.ToJsonString()}]"; + actual.DeepEquals(expect).Should().BeTrue(errorReason); + break; + } + } + } + + private string AppendQuery(string url) + { + var flUrl = new Url(url); + foreach (var query in this.ScenarioContext.GetAllQueryString()) + { + flUrl.QueryParams.Add(query.Key, query.Value.Trim()); + + // 不能用 SetQueryParam,因為有多個相同的 querystring 如: filters,會後蓋前 + //url = url.SetQueryParam(query.Key, query.Value.Trim()); + } + + return flUrl.ToString(); + } + + [Then(@"預期回傳內容中路徑 ""(.*)"" 的""(.*)"" ""(.*)""")] + public void Then預期回傳內容中路徑的(string selectPath, string operationType, string expected) + { + var srcJsonNode = this.ScenarioContext.GetJsonNode(); + ContentShouldBe(srcJsonNode, selectPath, operationType, expected); + } + + [Then(@"預期回傳內容為")] + public void Then預期回傳內容為(string expected) + { + var actual = this.ScenarioContext.GetHttpResponseBody(); + JsonNode expectedJsonNode = JsonNode.Parse(actual); + JsonAssert.Equal(expected, actual, true); + } + + [Given(@"調用端已準備 Header 參數")] + public void Given調用端已準備Header參數(Table table) + { + foreach (var row in table.Rows) + { + foreach (var header in table.Header) + { + var value = row[header]; + if (value == "{{next-page-token}}") + { + value = this.ScenarioContext.GetNextPageToken(); + } + + this.ScenarioContext.AddHttpHeader(header, value); + } + } + } + + [Given(@"調用端已準備 Query 參數")] + public void Given調用端已準備Query參數(Table table) + { + foreach (var row in table.Rows) + { + foreach (var header in table.Header) + { + var value = row[header]; + this.ScenarioContext.AddQueryString(header, value); + } + } + } + + [Given(@"初始化測試伺服器")] + public void Given初始化測試伺服器(Table table) + { + var row = table.Rows.FirstOrDefault(); + + DateTimeOffset? now = null; + if (row.TryGetValue("Now", out var nowText)) + { + now = TestAssistant.ToUtc(nowText); + this.ScenarioContext.SetUtcNow(now); + } + + if (row.TryGetValue("UserId", out var userId)) + { + this.ScenarioContext.SetUserId(userId); + } + + var server = new TestServer(now.Value, userId); + var httpClient = server.CreateClient(); + this.ScenarioContext.SetHttpClient(httpClient); + this.ScenarioContext.SetServiceProvider(server.Services); + } + + [Given(@"調用端已準備 Body 參數\(Json\)")] + public void Given調用端已準備Body參數Json(string json) + { + this.ScenarioContext.SetHttpRequestBody(json); + } + + [Then(@"預期資料庫已存在 Member 資料為")] + public async Task Then預期資料庫已存在Member資料為(Table table) + { + await using var dbContext = await this.ScenarioContext.GetMemberDbContextFactory().CreateDbContextAsync(); + var actual = await dbContext.Members.AsNoTracking().ToListAsync(); + table.CompareToSet(actual); + } + + [Then(@"預期得到 HttpStatusCode 為 ""(.*)""")] + public void Then預期得到HttpStatusCode為(int expected) + { + var actual = (int)this.ScenarioContext.GetHttpStatusCode(); + actual.Should().Be(expected); + } + + [Then(@"預期得到 Header 為")] + public void Then預期得到Header為(Table table) + { + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/DB/Scripts/NewFile1.txt b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/DB/Scripts/NewFile1.txt new file mode 100644 index 00000000..e69de29b diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/DbContextExtensions.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/DbContextExtensions.cs new file mode 100644 index 00000000..ed14703e --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/DbContextExtensions.cs @@ -0,0 +1,60 @@ +using System.Data; +using Lab.Sharding.DB; +using Lab.Sharding.DB.AutoGenerated; +using Lab.Sharding.Testing.Common; +using Microsoft.EntityFrameworkCore; + +namespace JobBank1111.Job.WebAPI.IntegrationTest; + +public static class DbContextExtensions +{ + public static void ClearAllData(this MemberDbContext dbContext) + { + SqlServerGenerateScript.OnlySupportLocal(dbContext.Database.GetConnectionString()); + dbContext.Database.ExecuteSqlRaw(SqlServerGenerateScript.ClearAllRecord()); + } + + public static async Task Initial(this MemberDbContext dbContext) + { + SqlServerGenerateScript.OnlySupportLocal(dbContext.Database.GetConnectionString()); + // dbContext.Database.EnsureDeleted(); + + var migrations = dbContext.Database.GetMigrations(); + if (migrations != null && migrations.Any()) + { + dbContext.Database.Migrate(); + } + else + { + dbContext.Database.EnsureCreated(); + } + + await dbContext.Seed(); + } + + public static async Task Seed(this MemberDbContext dbContext) + { + SqlServerGenerateScript.OnlySupportLocal(dbContext.Database.GetConnectionString()); + + var dbConnection = dbContext.Database.GetDbConnection(); + if (dbConnection.State != ConnectionState.Open) + { + await dbConnection.OpenAsync(); + } + + //讀取資料夾的所有 sql 檔案,並執行 + var sqlFiles = Directory.GetFiles("DB/Scripts", "*.sql"); + + foreach (var sqlFile in sqlFiles) + { + var sql = await File.ReadAllTextAsync(sqlFile); + await using var cmd = dbConnection.CreateCommand(); + + // Iterate the string array and execute each one. + cmd.CommandText = sql; + await cmd.ExecuteNonQueryAsync(); + + // dbContext.Database.ExecuteSqlRaw(sql); + } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/Lab.Sharding.IntegrationTest.csproj b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/Lab.Sharding.IntegrationTest.csproj new file mode 100644 index 00000000..0c9e94a8 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/Lab.Sharding.IntegrationTest.csproj @@ -0,0 +1,50 @@ + + + + net8.0 + enable + enable + + false + + JobBank1111.Job.WebAPI.IntegrationTest + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + Always + + + + diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/ScenarioContextExtension.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/ScenarioContextExtension.cs new file mode 100644 index 00000000..9c97b87b --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/ScenarioContextExtension.cs @@ -0,0 +1,144 @@ +using System.Net; +using System.Text; +using System.Text.Json.Nodes; +using Lab.Sharding.DB; +using Lab.Sharding.DB.AutoGenerated; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Reqnroll; + +namespace JobBank1111.Job.WebAPI.IntegrationTest; + +public static class ScenarioContextExtension +{ + public static T? GetOrDefault(this ScenarioContext context, string key, T? defaultValue = default) => + context.ContainsKey(key) ? context.Get(key) : defaultValue; + + public static void SetServiceProvider(this ScenarioContext context, IServiceProvider serviceProvider) + { + context.Set(serviceProvider); + } + + public static IServiceProvider GetServiceProvider(this ScenarioContext context) + { + return context.Get(); + } + + public static IDbContextFactory GetMemberDbContextFactory(this ScenarioContext context) + { + return GetServiceProvider(context).GetService>(); + } + + public static string? GetUserId(this ScenarioContext context) + => context.TryGetValue($"UserId", out string userId) + ? userId + : null; + + public static void SetUserId(this ScenarioContext context, string userId) => context.Set(userId, $"UserId"); + + public static DateTimeOffset? GetUtcNow(this ScenarioContext context) => + context.TryGetValue($"UtcNow", out DateTimeOffset dateTime) + ? dateTime + : null; + + public static void SetUtcNow(this ScenarioContext context, DateTimeOffset? dateTime) => + context.Set(dateTime, $"UtcNow"); + + public static long? GetFirmId(this ScenarioContext context) => + context.TryGetValue($"FirmId", out long firmId) + ? firmId + : null; + + public static void SetHttpClient(this ScenarioContext context, HttpClient httpClient) => + context.Set(httpClient); + + public static HttpClient GetHttpClient(this ScenarioContext context) => + context.Get(); + + public static void AddQueryString(this ScenarioContext context, string key, string value) + { + if (!context.TryGetValue>("QueryString", out var data)) + { + data = new List<(string Key, string Value)>(); + } + + data.Add((key, value)); + context.Set(data, "QueryString"); + } + + public static IList<(string Key, string Value)> GetAllQueryString(this ScenarioContext context) + { + return context.TryGetValue(out IList<(string Key, string Value)> result) + ? result + : new List<(string Key, string Value)>(); + } + + public static void SetHttpResponse(this ScenarioContext context, HttpResponseMessage response) => + context.Set(response); + + public static HttpResponseMessage GetHttpResponse(this ScenarioContext context) => + context.TryGetValue(out HttpResponseMessage result) ? result : default; + + public static void SetHttpResponseBody(this ScenarioContext context, string body) => + context.Set(body, "HttpResponseBody"); + + public static string GetHttpResponseBody(this ScenarioContext context) => + context.TryGetValue("HttpResponseBody", out string body) ? body : null; + + public static void SetHttpStatusCode(this ScenarioContext context, HttpStatusCode httpStatusCode) => + context.Set(httpStatusCode, "HttpStatusCode"); + + public static HttpStatusCode GetHttpStatusCode(this ScenarioContext context) => + context.Get("HttpStatusCode"); + + public static void SetNextPageToken(this ScenarioContext context, string nextPageToken) => + context.Set(nextPageToken, "NextPageToken"); + + public static string GetNextPageToken(this ScenarioContext context) => + context.Get("NextPageToken"); + + public static void SetXUnitLog(this ScenarioContext context, StringBuilder stringBuilder) + { + context.Set(stringBuilder, "XUnitLog"); + } + + public static StringBuilder GetXUnitLog(this ScenarioContext context) + { + context.TryGetValue("XUnitLog", out StringBuilder? stringBuilder); + return stringBuilder ?? new StringBuilder(); + } + + public static void AddHttpHeader(this ScenarioContext context, string key, string value) + { + var headers = context.GetOrNewHeaders(); + headers[key] = value; + context.Set(headers); + } + + public static IHeaderDictionary GetOrNewHeaders(this ScenarioContext context) + { + if (context.TryGetValue(out IHeaderDictionary result) is false) + { + result = new HeaderDictionary(); + } + + return result; + } + + public static void SetHttpRequestBody(this ScenarioContext context, string body) => + context.Set(body, "HttpRequestBody"); + + public static string? GetHttpRequestBody(this ScenarioContext context) => + context.GetOrDefault($"HttpRequestBody"); + + public static void SetJsonNode(this ScenarioContext context, JsonNode input) + { + context.Set(input); + } + + public static JsonNode GetJsonNode(this ScenarioContext context) + { + return context.Get(); + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/ServiceCollectionExtension.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/ServiceCollectionExtension.cs new file mode 100644 index 00000000..16595558 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/ServiceCollectionExtension.cs @@ -0,0 +1,26 @@ +using Lab.Sharding.Infrastructure.TraceContext; +using Lab.Sharding.WebAPI; +using Microsoft.Extensions.DependencyInjection; + +namespace JobBank1111.Job.WebAPI.IntegrationTest; + +public static class ServiceCollectionExtension +{ + public static IServiceCollection AddFakeContextAccessor(this IServiceCollection services, string userId) + { + services.AddSingleton>(p => + { + var traceContext = new TraceContext + { + TraceId = "測試", + UserId = userId + }; + var accessor = new ContextAccessor(); + accessor.Set(traceContext); + return accessor; + }); + services.AddSingleton>(p => p.GetService>()); + services.AddSingleton>(p => p.GetService>()); + return services; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/TestAssistant.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/TestAssistant.cs new file mode 100644 index 00000000..a03b629b --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/TestAssistant.cs @@ -0,0 +1,40 @@ +using Lab.Sharding.WebAPI; + +namespace JobBank1111.Job.WebAPI.IntegrationTest; + +public enum MyEnum +{ + MyProperty = 0, +} + +class TestAssistant +{ + public static void SetEnvironmentVariables() + { + Environment.SetEnvironmentVariable("JOB1111_ENVIRONMENT", "QA"); + Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development"); + Environment.SetEnvironmentVariable(nameof(EXTERNAL_API), "http://localhost:5000/api"); + } + + public static void SetDbConnectionEnvironmentVariable(string connectionString) + { + Environment.SetEnvironmentVariable(nameof(SYS_DATABASE_CONNECTION_STRING1), connectionString); + } + + public static void SetRedisConnectionEnvironmentVariable(string url) + { + Environment.SetEnvironmentVariable(nameof(SYS_REDIS_URL), url); + } + + public static void SetExternalConnectionEnvironmentVariable(string url) + { + Environment.SetEnvironmentVariable(nameof(EXTERNAL_API), url); + } + + public static DateTime ToUtc(string time) + { + var tempTime = DateTimeOffset.Parse(time); + var utcTime = new DateTimeOffset(tempTime.DateTime, TimeSpan.Zero).UtcDateTime; + return utcTime; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/TestServer.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/TestServer.cs new file mode 100644 index 00000000..15dc8365 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/TestServer.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Time.Testing; +using Microsoft.VisualStudio.TestPlatform.TestHost; + +namespace JobBank1111.Job.WebAPI.IntegrationTest; + +public class TestServer(DateTimeOffset now, + string userId) + : WebApplicationFactory +{ + private void ConfigureServices(IServiceCollection services) + { + //模擬身分 + services.AddFakeContextAccessor(userId); + + //模擬現在時間 + var fakeTimeProvider = new FakeTimeProvider(now); + services.AddSingleton(fakeTimeProvider); + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureServices(this.ConfigureServices); + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/Usings.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/Usings.cs new file mode 100644 index 00000000..ab67c7ea --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git "a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/_01_Demo/\351\243\257\347\262\222.feature" "b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/_01_Demo/\351\243\257\347\262\222.feature" new file mode 100644 index 00000000..004bb0fd --- /dev/null +++ "b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/_01_Demo/\351\243\257\347\262\222.feature" @@ -0,0 +1,176 @@ +Feature: 飯粒 + + Background: + Given 調用端已準備 Header 參數 + | x-trace-id | + | TW | + Given 調用端已準備 Query 參數 + | select-profile | + | avatarUrl | + Given 初始化測試伺服器 + | Now | UserId | + | 2000-01-01T00:00:00+00:00 | yao | + + Scenario: 新增一筆會員 + Given 調用端已準備 Body 參數(Json) + """ + { + "email": "yao@9527", + "name": "yao", + "age": 18 + } + """ + When 調用端發送 "POST" 請求至 "api/v1/members" + Then 預期得到 HttpStatusCode 為 "204" + Then 預期資料庫已存在 Member 資料為 + | Email | Name | Age | CreatedAt | CreatedAt | + | yao@9527 | yao | 18 | 2000-01-01T00:00:00+00:00 | 2000-01-01T00:00:00+00:00 | + + Scenario: 查詢所有會員 offset + Given 資料庫已存在 Member 資料 + | Id | Email | Name | Age | + | 1 | yao@9527 | yao1 | 18 | + | 2 | yao@9528 | yao2 | 18 | + | 3 | yao@9529 | yao3 | 18 | + Given 調用端已準備 Header 參數 + | x-page-size | x-page-index | cache-control | + | 2 | 0 | no-cache | + When 調用端發送 "GET" 請求至 "api/v1/members:offset" + Then 預期得到 HttpStatusCode 為 "200" + Then 預期得到 Header 為 + | page-size | page-index | row-total | + | 2 | 0 | 3 | + Then 預期回傳內容為 + """ + { + "items": [ + { + "id": "1", + "name": "yao1", + "age": 18, + "email": "yao@9527", + "sequenceId": null + }, + { + "id": "2", + "name": "yao2", + "age": 18, + "email": "yao@9528", + "sequenceId": null + } + ], + "pageIndex": 0, + "totalPages": 2, + "hasPreviousPage": false, + "hasNextPage": true + } + """ + + Scenario: 查詢所有會員 cursor + Given 資料庫已存在 Member 資料 + | Id | Email | Name | Age | + | 1 | yao@9527 | yao1 | 18 | + | 2 | yao@9528 | yao2 | 18 | + | 3 | yao@9529 | yao3 | 18 | + Given 調用端已準備 Header 參數 + | x-page-size | x-next-page-token | + | 1 | | + When 調用端發送 "GET" 請求至 "api/v1/members:cursor" + Then 預期得到 HttpStatusCode 為 "200" + Then 預期回傳內容為 + """ + { + "items": [ + { + "id": "1", + "name": "yao1", + "age": 18, + "email": "yao@9527", + "sequenceId": 1 + } + ], + "nextPageToken": "eyJsYXN0SWQiOiIxIiwibGFzdFNlcXVlbmNlSWQiOjF9", + "nextPreviousToken": null + } + """ + Then 預期得到 HttpStatusCode 為 "200" + Given 調用端已準備 Header 參數 + | x-page-size | x-next-page-token | + | 1 | {{next-page-token}} | + When 調用端發送 "GET" 請求至 "api/v1/members:cursor" + Then 預期得到 HttpStatusCode 為 "200" + Then 預期回傳內容為 + """ + { + "items": [ + { + "id": "2", + "name": "yao2", + "age": 18, + "email": "yao@9528", + "sequenceId": 2 + } + ], + "nextPageToken": "eyJsYXN0SWQiOiIyIiwibGFzdFNlcXVlbmNlSWQiOjJ9", + "nextPreviousToken": null + } + """ + + Scenario: 外部服務 + Given 資料庫已存在 Member 資料 + | Id | Email | Name | Age | + | 1 | yao@9527 | yao1 | 18 | + Given 建立假端點,HttpMethod = "POST",URL = "/ec/V1/SalePage/UpdateStock",StatusCode = "200",ResponseContent = + """ + { + "ErrorId": "", + "Status": "Success", + "Data": "", + "ErrorMessage": null, + "TimeStamp": "2024-02-21T16:55:21.4988154+08:00" + } + """ + + Scenario: 用 JsonDiff 驗證資料 + When 模擬呼叫 API,得到以下內容 + """ + { + "Id": 1, + "Age": 18, + "Birthday": "2000-01-01T00:00:00+00:00", + "FullName": { + "FirstName": "John", + "LastName": "Doe" + } + } + """ + Then 預期回傳內容為 + """ + { + "Id": 1, + "Age": 18, + "Birthday": "2000-01-01T00:00:00+00:00", + "FullName": { + "FirstName": "John", + "LastName": "Doe" + } + } + """ + + Scenario: 用 JsonPath 驗證資料 + Given 已存在 Json 內容 + """ + { + "Id": 1, + "Age": 18, + "Birthday": "2000-01-01T00:00:00+00:00", + "FullName": { + "FirstName": "John", + "LastName": "Doe" + } + } + """ + Then 預期回傳內容中路徑 "$.Age" 的"數值等於" "18" + Then 預期回傳內容中路徑 "$.Birthday" 的"時間等於" "2000-01-01T00:00:00+00:00" + Then 預期回傳內容中路徑 "$.FullName.FirstName" 的"字串等於" "John" + Then 預期回傳內容中路徑 "$.FullName.LastName" 的"字串等於" "Doe" \ No newline at end of file diff --git "a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/_01_Demo/\351\243\257\347\262\222Step.cs" "b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/_01_Demo/\351\243\257\347\262\222Step.cs" new file mode 100644 index 00000000..eec52713 --- /dev/null +++ "b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/_01_Demo/\351\243\257\347\262\222Step.cs" @@ -0,0 +1,21 @@ +using System.Text.Json.Nodes; +using Reqnroll; + +namespace JobBank1111.Job.WebAPI.IntegrationTest._01_Demo; + +[Binding] +public class 飯粒Step : Steps +{ + [Given(@"已存在 Json 內容")] + public void Given已存在Json內容(string json) + { + var jsonNode = JsonNode.Parse(json); + this.ScenarioContext.SetJsonNode(jsonNode); + } + + [When(@"模擬呼叫 API,得到以下內容")] + public void When模擬呼叫api得到以下內容(string json) + { + this.ScenarioContext.SetHttpResponseBody(json); + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Test/Lab.Sharding.Test.csproj b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Test/Lab.Sharding.Test.csproj new file mode 100644 index 00000000..6831b96d --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Test/Lab.Sharding.Test.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Test/UnitTest1.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Test/UnitTest1.cs new file mode 100644 index 00000000..e3d2be2c --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Test/UnitTest1.cs @@ -0,0 +1,19 @@ +using Lab.Sharding.WebAPI; +using Lab.Sharding.WebAPI.Contract; +using Newtonsoft.Json; +using JsonSerializer = System.Text.Json.JsonSerializer; + +namespace Lab.Sharding.Test; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + var data = new Lab.Sharding.WebAPI.PaginatedList(); + + var json = "{\"items\":[{\"id\":\"1a6bd961-7a8b-470d-9282-2420c1daa211\",\"name\":\"yao\",\"age\":20,\"email\":\"yao@9527\"},{\"id\":\"93737ff9-a162-4d2e-92ac-b1944f5cce90\",\"name\":\"yao2\",\"age\":18,\"email\":\"9527@yao\"}],\"pageIndex\":0,\"totalPages\":1,\"hasPreviousPage\":false,\"hasNextPage\":true}"; + var paginatedList = JsonSerializer.Deserialize>(json); + var deserializeObject = JsonConvert.DeserializeObject>(json); + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/Lab.Sharding.Testing.Common.csproj b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/Lab.Sharding.Testing.Common.csproj new file mode 100644 index 00000000..511def9f --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/Lab.Sharding.Testing.Common.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + + + + + + + + + + diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/Cookies.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/Cookies.cs new file mode 100644 index 00000000..f138ef3b --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/Cookies.cs @@ -0,0 +1,6 @@ +namespace Lab.Sharding.Testing.Common.MockServer.Contracts; + +public class Cookies +{ + public string Session { get; set; } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/HttpRequest.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/HttpRequest.cs new file mode 100644 index 00000000..78b327b5 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/HttpRequest.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Lab.Sharding.Testing.Common.MockServer.Contracts; + +public class HttpRequest +{ + public string Method { get; set; } + public string Path { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Cookies Cookies { get; set; } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/HttpResponse.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/HttpResponse.cs new file mode 100644 index 00000000..846e40d5 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/HttpResponse.cs @@ -0,0 +1,8 @@ +namespace Lab.Sharding.Testing.Common.MockServer.Contracts; + +public class HttpResponse +{ + public string Body { get; set; } + + public int StatusCode { get; set; } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/PutNewEndPointRequest.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/PutNewEndPointRequest.cs new file mode 100644 index 00000000..d8127ba4 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/PutNewEndPointRequest.cs @@ -0,0 +1,7 @@ +namespace Lab.Sharding.Testing.Common.MockServer.Contracts; + +public class PutNewEndPointRequest +{ + public HttpRequest HttpRequest { get; set; } + public HttpResponse HttpResponse { get; set; } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/MockServerHelper.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/MockServerHelper.cs new file mode 100644 index 00000000..482a4d34 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/MockServerHelper.cs @@ -0,0 +1,68 @@ +// using System.Text; +// using System.Text.Json; +// using DotNet.Testcontainers.Builders; +// using DotNet.Testcontainers.Containers; +// using Lab.Sharding.Testing.Common.MockServers.Contracts; +// +// namespace Lab.Sharding.Testing.Common.MockServers; +// +// public static class MockServerProvider +// { +// private const int internalPort = 1080; +// public static int Port => 55124; +// private static string Image => "mockserver/mockserver"; +// public static string Hostname => $"http://{_container.Value.Hostname}:{Port}"; +// +// public static IContainer CreateContainer() => _container.Value; +// +// private static Lazy _container = new(() => new ContainerBuilder().WithImage(Image).WithPortBinding(Port,internalPort).Build()); +// private static Lazy _httpClient = new(() => new HttpClient +// { +// BaseAddress = new Uri(Hostname) +// }); +// +// public static async Task PutNewEndPoint(string httpMethod, string relativePath, int statusCode, string source) +// { +// +// var client = _httpClient.Value!; +// +// var requestModel = new PutNewEndPointRequest +// { +// HttpRequest = new HttpRequest +// { +// Method = httpMethod.ToUpper(), +// Path = relativePath.StartsWith("/") ? relativePath : $"/{relativePath}", +// }, +// HttpResponse =new HttpResponse +// { +// Body = source, +// StatusCode = statusCode +// } +// }; +// +// var content = JsonSerializer.Serialize( +// requestModel, +// new JsonSerializerOptions +// { +// +// PropertyNamingPolicy = JsonNamingPolicy.CamelCase, +// }); +// +// using var request = new HttpRequestMessage(HttpMethod.Put, $"{Hostname}/mockserver/expectation") +// { +// Content = new StringContent(content, Encoding.UTF8, "application/json") +// }; +// +// using var response = await client.SendAsync(request); +// +// response.EnsureSuccessStatusCode(); +// } +// +// public static async Task ResetAsync() +// { +// var client = _httpClient.Value!; +// using var request = new HttpRequestMessage(HttpMethod.Put, $"{Hostname}/mockserver/reset"); +// using var response = await client.SendAsync(request); +// response.EnsureSuccessStatusCode(); +// } +// } \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/MockedServerAssistant.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/MockedServerAssistant.cs new file mode 100644 index 00000000..76ca7aa4 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/MockedServerAssistant.cs @@ -0,0 +1,45 @@ +using System.Text; +using System.Text.Json; +using Lab.Sharding.Testing.Common.MockServer.Contracts; + +namespace Lab.Sharding.Testing.Common.MockServer; + +public class MockedServerAssistant +{ + public static async Task ResetAsync(HttpClient client) + { + using var request = new HttpRequestMessage(HttpMethod.Put, $"mockserver/reset"); + using var response = await client.SendAsync(request); + response.EnsureSuccessStatusCode(); + } + + public static async Task PutNewEndPointAsync(HttpClient client, + string httpMethod, + string relativePath, + int statusCode, + string body) + { + var requestModel = new PutNewEndPointRequest + { + HttpRequest = new HttpRequest + { + Method = httpMethod.ToUpper(), + Path = relativePath.StartsWith("/") ? relativePath : $"/{relativePath}", + }, + HttpResponse = new HttpResponse { Body = body, StatusCode = statusCode } + }; + + var content = JsonSerializer.Serialize( + requestModel, + new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }); + + using var request = new HttpRequestMessage(HttpMethod.Put, $"mockserver/expectation") + { + Content = new StringContent(content, Encoding.UTF8, "application/json") + }; + + using var response = await client.SendAsync(request); + + response.EnsureSuccessStatusCode(); + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/NpgsqlGenerateScript.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/NpgsqlGenerateScript.cs new file mode 100644 index 00000000..2eb87480 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/NpgsqlGenerateScript.cs @@ -0,0 +1,58 @@ +namespace Lab.Sharding.Testing.Common; + +public class NpgsqlGenerateScript +{ + public static string ClearAllRecord() + { + return @" +DO $$ +DECLARE row RECORD; +DECLARE seq TEXT; +BEGIN + FOR row IN SELECT table_name + FROM information_schema.tables + WHERE table_type='BASE TABLE' + AND table_schema='public' + AND table_name NOT IN ('admins', 'admin_roles', '__EFMigrationsHistory') + LOOP + EXECUTE format('TRUNCATE TABLE %I CONTINUE IDENTITY CASCADE;', row.table_name); + END LOOP; + FOR row IN SELECT table_name + FROM information_schema.tables + WHERE table_type='BASE TABLE' + AND table_schema='histories' + AND table_name NOT IN ('admins', 'admin_roles', '__EFMigrationsHistory') + LOOP + EXECUTE format('TRUNCATE TABLE histories.%I CONTINUE IDENTITY CASCADE;', row.table_name); + END LOOP; + FOR seq IN (select sequencename FROM pg_sequences) LOOP + EXECUTE 'ALTER SEQUENCE IF EXISTS '||seq||' RESTART WITH 1;'; + END LOOP; +END; +$$; +"; + } + + public static string CreateChannelIdSeq() + { + return "Create SEQUENCE channel_channel_id_seq RESTART WITH 1;"; + } + + public static string ReseedChannelIdSeq() + { + return "ALTER SEQUENCE channel_channel_id_seq RESTART WITH 1;"; + } + + // public static void OnlySupportLocal(string connectionString) + // { + // var builder = new NpgsqlConnectionStringBuilder(connectionString); + // if (string.Compare(builder.Host, "LOCALHOST", StringComparison.InvariantCultureIgnoreCase) != 0 + // && string.Compare(builder.Host, "127.0.0.1", StringComparison.InvariantCultureIgnoreCase) != 0 + // && string.Compare(builder.Host, "172.17.0.1", StringComparison.InvariantCultureIgnoreCase) != + // 0 // docker 建立容器時的預設位置 + // ) + // { + // throw new NotSupportedException($"伺服器只支援 localhost,目前連線字串為 {connectionString}"); + // } + // } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/RedirectConsole.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/RedirectConsole.cs new file mode 100644 index 00000000..353842ac --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/RedirectConsole.cs @@ -0,0 +1,22 @@ +namespace Lab.Sharding.Testing.Common; + +public sealed class RedirectConsole : IDisposable +{ + private readonly Action _logFunction; + private readonly TextWriter _oldOut = Console.Out; + private readonly StringWriter sw = new StringWriter(); + + public RedirectConsole(Action logFunction) + { + this._logFunction = logFunction; + Console.SetOut(this.sw); + } + + public void Dispose() + { + Console.SetOut(this._oldOut); + this.sw.Flush(); + this._logFunction(this.sw.ToString()); + this.sw.Dispose(); + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/SqlServerGenerateScript.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/SqlServerGenerateScript.cs new file mode 100644 index 00000000..5d8d380e --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/SqlServerGenerateScript.cs @@ -0,0 +1,32 @@ +using Microsoft.Data.SqlClient; + +namespace Lab.Sharding.Testing.Common; + +public class SqlServerGenerateScript +{ + public static string ClearAllRecord() + { + return @" +EXEC sp_MSForEachTable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL' +EXEC sp_MSForEachTable 'SET QUOTED_IDENTIFIER ON; DELETE FROM ?' +EXEC sp_MSForEachTable 'ALTER TABLE ? WITH CHECK CHECK CONSTRAINT ALL' +"; + } + + public static void OnlySupportLocal(string connectionString) + { + var allowData = new List() + { + "localhost", + "127.0.0.1", + "172.17.0.1" //localhost in docker + }; + var builder = new SqlConnectionStringBuilder(connectionString); + var dataSource = builder.DataSource.Split(',')[0]; // Extract the IP part + var contains = allowData.Contains(dataSource); + if (contains == false) + { + throw new NotSupportedException($"伺服器只支援 localhost,目前連線字串為 {connectionString}"); + } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/TestContainerFactory.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/TestContainerFactory.cs new file mode 100644 index 00000000..549b17f5 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/TestContainerFactory.cs @@ -0,0 +1,66 @@ +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; +using Testcontainers.MsSql; +using Testcontainers.PostgreSql; +using Testcontainers.Redis; + +namespace Lab.Sharding.Testing.Common; + +public class TestContainerFactory +{ + // TODO:docker hub 有訪問次數限制,需要一台 proxy server + public static async Task CreateRedisContainerAsync() + { + var redisContainer = new RedisBuilder() + .WithImage("redis:7.0") + .Build(); + await redisContainer.StartAsync(); + return redisContainer; + } + + public static async Task CreateMsSqlContainerAsync() + { + var container = new MsSqlBuilder() + .WithName("sql2019") + .WithImage("mcr.microsoft.com/mssql/server:2019-latest") + .WithPassword("pass@w0rd1~") + .WithEnvironment("ACCEPT_EULA", "Y") + .WithEnvironment("MSSQL_PID", "Developer") + .WithPortBinding(1433, assignRandomHostPort: true) + .Build(); + await container.StartAsync(); + return container; + } + + public static async Task CreatePostgreSqlContainerAsync() + { + var waitStrategy = Wait.ForUnixContainer().UntilCommandIsCompleted("pg_isready"); + var container = new PostgreSqlBuilder() + .WithImage("postgres:13-alpine") + .WithName("postgres.13") + .WithPortBinding(5432, assignRandomHostPort: true) + .WithWaitStrategy(waitStrategy) + .WithUsername("postgres") + .WithPassword("postgres") + .Build(); + await container.StartAsync(); + return container; + } + + public static async Task CreateMockServerContainerAsync() + { + var container = new ContainerBuilder() + .WithName("mockserver") + .WithImage("mockserver/mockserver") + .WithPortBinding(1080, assignRandomHostPort: true) + .Build(); + await container.StartAsync(); + return container; + } + + public static string GetMockServerConnection(IContainer container) + { + var port = container.GetMappedPublicPort(1080); + return $"http://{container.Hostname}:{port}"; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/CacheKeys.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/CacheKeys.cs new file mode 100644 index 00000000..d96e5a30 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/CacheKeys.cs @@ -0,0 +1,6 @@ +namespace Lab.Sharding.WebAPI; + +public enum CacheKeys +{ + MemberData, +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Contract/AutoGenerated/ContractControllers.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Contract/AutoGenerated/ContractControllers.cs new file mode 100644 index 00000000..43ab2c0f --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Contract/AutoGenerated/ContractControllers.cs @@ -0,0 +1,213 @@ +//---------------------- +// +// Generated using the NSwag toolchain v14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org) +// +//---------------------- + +#pragma warning disable 108 // Disable "CS0108 '{derivedDto}.ToJson()' hides inherited member '{dtoBase}.ToJson()'. Use the new keyword if hiding was intended." +#pragma warning disable 114 // Disable "CS0114 '{derivedDto}.RaisePropertyChanged(String)' hides inherited member 'dtoBase.RaisePropertyChanged(String)'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword." +#pragma warning disable 472 // Disable "CS0472 The result of the expression is always 'false' since a value of type 'Int32' is never equal to 'null' of type 'Int32?' +#pragma warning disable 612 // Disable "CS0612 '...' is obsolete" +#pragma warning disable 649 // Disable "CS0649 Field is never assigned to, and will always have its default value null" +#pragma warning disable 1573 // Disable "CS1573 Parameter '...' has no matching param tag in the XML comment for ... +#pragma warning disable 1591 // Disable "CS1591 Missing XML comment for publicly visible type or member ..." +#pragma warning disable 8073 // Disable "CS8073 The result of the expression is always 'false' since a value of type 'T' is never equal to 'null' of type 'T?'" +#pragma warning disable 3016 // Disable "CS3016 Arrays as attribute arguments is not CLS-compliant" +#pragma warning disable 8603 // Disable "CS8603 Possible null reference return" +#pragma warning disable 8604 // Disable "CS8604 Possible null reference argument for parameter" +#pragma warning disable 8625 // Disable "CS8625 Cannot convert null literal to non-nullable reference type" +#pragma warning disable 8765 // Disable "CS8765 Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes)." + +namespace Lab.Sharding.WebAPI.Contract +{ + using System = global::System; + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public interface ITagController + { + + /// next page token + + /// OK + + System.Threading.Tasks.Task> GetTagsCursorAsync(string x_next_page_token, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + + public partial class TagController : Microsoft.AspNetCore.Mvc.ControllerBase + { + private ITagController _implementation; + + public TagController(ITagController implementation) + { + _implementation = implementation; + } + + /// next page token + /// OK + [Microsoft.AspNetCore.Mvc.HttpGet, Microsoft.AspNetCore.Mvc.Route("api/v2/tags:cursor")] + public System.Threading.Tasks.Task> GetTagsCursor([Microsoft.AspNetCore.Mvc.FromHeader(Name = "x-next-page-token")] string x_next_page_token, System.Threading.CancellationToken cancellationToken) + { + + return _implementation.GetTagsCursorAsync(x_next_page_token, cancellationToken); + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public interface IMemberController + { + + /// OK + + System.Threading.Tasks.Task> GetMembersCursorAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + + /// OK + + System.Threading.Tasks.Task> GetMemberOffsetAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + + /// OK + + System.Threading.Tasks.Task InsertMember1Async(InsertMemberRequest body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + + public partial class MemberController : Microsoft.AspNetCore.Mvc.ControllerBase + { + private IMemberController _implementation; + + public MemberController(IMemberController implementation) + { + _implementation = implementation; + } + + /// OK + [Microsoft.AspNetCore.Mvc.HttpGet, Microsoft.AspNetCore.Mvc.Route("api/v2/members:cursor")] + public System.Threading.Tasks.Task> GetMembersCursor(System.Threading.CancellationToken cancellationToken) + { + + return _implementation.GetMembersCursorAsync(cancellationToken); + } + + /// OK + [Microsoft.AspNetCore.Mvc.HttpGet, Microsoft.AspNetCore.Mvc.Route("api/v2/members:offset")] + public System.Threading.Tasks.Task> GetMemberOffset(System.Threading.CancellationToken cancellationToken) + { + + return _implementation.GetMemberOffsetAsync(cancellationToken); + } + + /// OK + [Microsoft.AspNetCore.Mvc.HttpPost, Microsoft.AspNetCore.Mvc.Route("api/v2/members")] + public System.Threading.Tasks.Task InsertMember1([Microsoft.AspNetCore.Mvc.FromBody] InsertMemberRequest body, System.Threading.CancellationToken cancellationToken) + { + + return _implementation.InsertMember1Async(body, cancellationToken); + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class GetMemberResponse + { + + [System.Text.Json.Serialization.JsonPropertyName("id")] + public string Id { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("name")] + public string Name { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("age")] + public int? Age { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("email")] + public string Email { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("sequenceId")] + public long? SequenceId { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class GetMemberResponseCursorPaginatedList + { + + [System.Text.Json.Serialization.JsonPropertyName("items")] + public System.Collections.Generic.List Items { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("nextPageToken")] + public string NextPageToken { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("nextPreviousToken")] + public string NextPreviousToken { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class GetMemberResponsePaginatedList + { + + [System.Text.Json.Serialization.JsonPropertyName("items")] + public System.Collections.Generic.List Items { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("pageIndex")] + public int PageIndex { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("totalPages")] + public int TotalPages { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("hasPreviousPage")] + public bool HasPreviousPage { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("hasNextPage")] + public bool HasNextPage { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class InsertMemberRequest + { + + [System.Text.Json.Serialization.JsonPropertyName("email")] + public string Email { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("name")] + public string Name { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("age")] + public int Age { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Failure + { + + [System.Text.Json.Serialization.JsonPropertyName("code")] + public string Code { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("reason")] + public string Reason { get; set; } + + } + + +} + +#pragma warning restore 108 +#pragma warning restore 114 +#pragma warning restore 472 +#pragma warning restore 612 +#pragma warning restore 1573 +#pragma warning restore 1591 +#pragma warning restore 8073 +#pragma warning restore 3016 +#pragma warning restore 8603 +#pragma warning restore 8604 +#pragma warning restore 8625 \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/CursorPaginatedList.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/CursorPaginatedList.cs new file mode 100644 index 00000000..f3e8693a --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/CursorPaginatedList.cs @@ -0,0 +1,17 @@ +namespace Lab.Sharding.WebAPI; + +public class CursorPaginatedList +{ + public List Items { get; } + + public string NextPageToken { get; set; } + + public string NextPreviousToken { get; set; } + + public CursorPaginatedList(List items, string nextPageToken, string nextPreviousToken) + { + this.Items = items; + this.NextPageToken = nextPageToken; + this.NextPreviousToken = nextPreviousToken; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/EnvironmentVariables.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/EnvironmentVariables.cs new file mode 100644 index 00000000..88ba33d7 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/EnvironmentVariables.cs @@ -0,0 +1,13 @@ +using Lab.Sharding.Infrastructure; + +namespace Lab.Sharding.WebAPI; + +public record ASPNETCORE_ENVIRONMENT : EnvironmentVariableBase; + +public record SYS_DATABASE_CONNECTION_STRING1 : EnvironmentVariableBase; + +public record SYS_DATABASE_CONNECTION_STRING2 : EnvironmentVariableBase; + +public record SYS_REDIS_URL : EnvironmentVariableBase; + +public record EXTERNAL_API : EnvironmentVariableBase; \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Failure.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Failure.cs new file mode 100644 index 00000000..600f265b --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Failure.cs @@ -0,0 +1,44 @@ +using System.Text.Json.Serialization; + +namespace Lab.Sharding.WebAPI; + +public class Failure +{ + public Failure() + { + } + + public Failure(string code, string message) + { + this.Code = code; + this.Message = message; + } + + /// + /// 錯誤碼 + /// + public string Code { get; init; } + + /// + /// 錯誤訊息 + /// + public string Message { get; init; } + + /// + /// 錯誤發生時的資料 + /// + public object Data { get; init; } + + /// + /// 追蹤 Id + /// + public string TraceId { get; set; } + + /// + /// 例外,不回傳給 Web API + /// + [JsonIgnore] + public Exception Exception { get; set; } + + public List Details { get; init; } = new(); +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/FailureCode.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/FailureCode.cs new file mode 100644 index 00000000..16dd06ba --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/FailureCode.cs @@ -0,0 +1,8 @@ +namespace Lab.Sharding.WebAPI; + +public enum FailureCode +{ + Unauthorized, + DbError, + DuplicateEmail +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Lab.Sharding.WebAPI.csproj b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Lab.Sharding.WebAPI.csproj new file mode 100644 index 00000000..c497dabf --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Lab.Sharding.WebAPI.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/GetMemberResponse.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/GetMemberResponse.cs new file mode 100644 index 00000000..d58b3a9e --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/GetMemberResponse.cs @@ -0,0 +1,14 @@ +namespace Lab.Sharding.WebAPI.Member; + +public class GetMemberResponse +{ + public string Id { get; set; } + + public string? Name { get; set; } + + public int? Age { get; set; } + + public string Email { get; set; } + + public long? SequenceId { get; set; } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/InsertMemberRequest.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/InsertMemberRequest.cs new file mode 100644 index 00000000..b02c2e87 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/InsertMemberRequest.cs @@ -0,0 +1,10 @@ +namespace Lab.Sharding.WebAPI.Member; + +public class InsertMemberRequest +{ + public string Email { get; set; } + + public string Name { get; set; } + + public int Age { get; set; } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/Member.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/Member.cs new file mode 100644 index 00000000..df8f15cd --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/Member.cs @@ -0,0 +1,20 @@ +namespace Lab.Sharding.WebAPI.Member; + +public class Member +{ + public string Id { get; set; } + + public string? Name { get; set; } + + public int? Age { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public string? CreatedBy { get; set; } + + public DateTimeOffset? ChangedAt { get; set; } + + public string? ChangedBy { get; set; } + + public string Email { get; set; } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberChain.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberChain.cs new file mode 100644 index 00000000..01ac6157 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberChain.cs @@ -0,0 +1,66 @@ +using CSharpFunctionalExtensions; +using Lab.Sharding.Infrastructure.TraceContext; + +namespace Lab.Sharding.WebAPI.Member; + +public static class MemberChain +{ + + // 檢查是否有重複的 Email + public static Result + ValidateEmail(this Result previousResult, + InsertMemberRequest dest) + { + if (previousResult.IsFailure) + { + return previousResult; + } + + var src = previousResult.Value; + if (src == null) + { + return Result.Success(src); + } + + if (src.Email == dest.Email) + { + return Result.Failure(new Failure + { + Code = nameof(FailureCode.DuplicateEmail), + Message = "Email 重複", + Data = src, + }); + } + + return Result.Success(src); + } + + // 檢查是否有重複的 Email + public static Result + ValidateName(this Result previousResult, + InsertMemberRequest dest) + { + if (previousResult.IsFailure) + { + return previousResult; + } + + var src = previousResult.Value; + if (src == null) + { + return Result.Success(src); + } + + if (src.Email == dest.Email) + { + return Result.Failure(new Failure + { + Code = nameof(FailureCode.DuplicateEmail), + Message = "Email 重複", + Data = src, + }); + } + + return Result.Success(src); + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberController.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberController.cs new file mode 100644 index 00000000..e12cfc13 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberController.cs @@ -0,0 +1,81 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Lab.Sharding.WebAPI.Member; + +[ApiController] +public class MemberV1Controller( + MemberHandler memberHandler) : ControllerBase +{ + // [HttpGet] + // [Route("api/v1/members:cursor", Name = "GetMemberCursor")] + // public async Task>> GetMemberCursor( + // CancellationToken cancel = default) + // { + // var noCache = true; + // var pageSize = this.TryGetPageSize(); + // var nextPageToken = this.TryGetPageToken(); + // var result = await memberHandler.GetMembersAsync(pageSize, nextPageToken, noCache, cancel); + // return this.Ok(result); + // } + + [HttpGet] + [Route("api/v1/members:offset", Name = "GetMemberOffset")] + public async Task>> GetMemberOffset( + CancellationToken cancel = default) + { + var pageSize = 10; + var pageIndex = 0; + var noCache = true; + if (this.Request.Headers.TryGetValue("x-page-index", out var pageIndexText)) + { + int.TryParse(pageIndexText, out pageIndex); + } + + if (this.Request.Headers.TryGetValue("x-page-size", out var pageSizeText)) + { + int.TryParse(pageSizeText, out pageSize); + } + + if (this.Request.Headers.TryGetValue("cache-control", out var noCacheText)) + { + bool.TryParse(noCacheText, out noCache); + } + + var result = await memberHandler.GetMembersAsync(pageIndex, pageSize, noCache, cancel); + return this.Ok(result); + } + + // [HttpPost] + // [Route("api/v1/members", Name = "InsertMember1")] + // public async Task InsertMemberAsync(InsertMemberRequest request, + // CancellationToken cancel = default) + // { + // var result = await memberHandler.InsertAsync(request, cancel); + // if (result.IsFailure) + // { + // if (result.TryGetError(out var failure)) + // { + // return this.BadRequest(failure); + // } + // } + // + // return this.Ok(result.Value); + // } + + private int TryGetPageSize() + { + return this.Request.Headers.TryGetValue("x-page-size", out var pageSize) + ? int.Parse(pageSize.FirstOrDefault() ?? string.Empty) + : 10; + } + + private string TryGetPageToken() + { + if (this.Request.Headers.TryGetValue("x-next-page-token", out var nextPageToken)) + { + return nextPageToken; + } + + return null; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberHandler.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberHandler.cs new file mode 100644 index 00000000..059c9a5f --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberHandler.cs @@ -0,0 +1,125 @@ +using CSharpFunctionalExtensions; +using Lab.Sharding.Infrastructure.TraceContext; + +namespace Lab.Sharding.WebAPI.Member; + +public class MemberHandler( + MemberRepository repository, + IContextGetter traceContextGetter, + ILogger logger) +{ + // public async Task> + // InsertAsync(InsertMemberRequest request, + // CancellationToken cancel = default) + // { + // var traceContext = traceContextGetter.Get(); + // var srcMember = await repository.QueryEmailAsync(request.Email, cancel); + // + // //前置條件檢查,可以用 Fluent Pattern 重構 + // var validateResult = Result.Success(srcMember); + // validateResult = ValidateEmail(validateResult, request); + // validateResult = ValidateName(validateResult, request); + // if (validateResult.IsFailure) + // { + // return validateResult; + // } + // + // try + // { + // var count = await repository.InsertAsync(request, cancel); + // var success = Result.Success(srcMember); + // return success; + // + // //發送 Event 給 MQ + // } + // catch (Exception e) + // { + // //各自處理例外,處理過就不要再次 throw + // //模擬插資料失敗 + // var failure = new Failure + // { + // Code = nameof(FailureCode.DbError), + // Message = "資料庫錯誤", + // Data = request, + // Exception = e, + // TraceId = traceContext.TraceId + // }; + // + // logger.LogError($"{failure}", e); + // var failed = Result.Failure(failure); + // return failed; + // } + // } + + // 檢查是否有重複的 Email + private static Result + ValidateEmail(Result previousResult, + InsertMemberRequest dest) + { + if (previousResult.IsFailure) + { + return previousResult; + } + + var src = previousResult.Value; + if (src == null) + { + return Result.Success(src); + } + + if (src.Email == dest.Email) + { + return Result.Failure(new Failure + { + Code = nameof(FailureCode.DuplicateEmail), + Message = "Email 重複", + Data = src, + }); + } + + return Result.Success(src); + } + + // 檢查是否有重複的 Email + private static Result + ValidateName(Result previousResult, + InsertMemberRequest dest) + { + if (previousResult.IsFailure) + { + return previousResult; + } + + var src = previousResult.Value; + if (src == null) + { + return Result.Success(src); + } + + if (src.Email == dest.Email) + { + return Result.Failure(new Failure + { + Code = nameof(FailureCode.DuplicateEmail), + Message = "Email 重複", + Data = src, + }); + } + + return Result.Success(src); + } + + public async Task> + GetMembersAsync(int pageIndex, int pageSize, bool noCache = true, CancellationToken cancel = default) + { + var result = await repository.GetMembersAsync(pageIndex, pageSize, noCache, cancel); + return result; + } + + // public async Task> + // GetMembersAsync(int pageSize, string nextPageToken, bool noCache = true, CancellationToken cancel = default) + // { + // var result = await repository.GetMembersAsync(pageSize, nextPageToken, noCache, cancel); + // return result; + // } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberRepository.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberRepository.cs new file mode 100644 index 00000000..ed221a84 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberRepository.cs @@ -0,0 +1,207 @@ +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using Lab.Sharding.Infrastructure; +using Lab.Sharding.Infrastructure.TraceContext; +using Lab.Sharding.DB; +using Lab.Sharding.DB.AutoGenerated; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Distributed; + +namespace Lab.Sharding.WebAPI.Member; + +public class MemberRepository( + ILogger logger, + IContextGetter contextGetter, + IDbContextFactory dbContextFactory, + IDynamicDbContextFactory dynamicDbContextFactory, + TimeProvider timeProvider, + IUuidProvider uuidProvider, + IDistributedCache cache, + JsonSerializerOptions jsonSerializerOptions) +{ + // public async Task InsertAsync(InsertMemberRequest request, + // CancellationToken cancel = default) + // { + // // throw new DbUpdateConcurrencyException("資料衝突了"); + // + // var now = timeProvider.GetUtcNow(); + // var traceContext = contextGetter.Get(); + // var userId = traceContext.UserId; + // await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancel); + // var toDb = new DB.AutoGenerated.Entities.Member + // { + // Id = uuidProvider.NewId(), + // Name = request.Name, + // Age = request.Age, + // Email = request.Email, + // CreatedAt = now, + // CreatedBy = userId, + // ChangedAt = now, + // ChangedBy = userId + // }; + // var entityEntry = dbContext.Members.Add(toDb); + // return await dbContext.SaveChangesAsync(cancel); + // } + // + // public async Task QueryEmailAsync(string email, + // CancellationToken cancel = default) + // { + // await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancel); + // var query = dbContext.Members + // .Where(p => p.Email == email) + // .Select(p => new Member + // { + // Id = p.Id, + // Name = p.Name, + // Age = p.Age, + // CreatedAt = p.CreatedAt, + // CreatedBy = p.CreatedBy, + // ChangedAt = p.ChangedAt, + // ChangedBy = p.ChangedBy + // }); + // + // var result = await query.TagWith($"{nameof(MemberRepository)}.{nameof(this.QueryEmailAsync)}({email})") + // .AsNoTracking() + // .FirstOrDefaultAsync(cancel); + // return result; + // } + + public async Task> + GetMembersAsync(int pageIndex, int pageSize, bool noCache = false, CancellationToken cancel = default) + { + var traceContext = contextGetter.Get(); + var userId = traceContext.UserId; + PaginatedList result; + var key = nameof(CacheKeys.MemberData); + string cachedData = null; + if (noCache == false) // 如果有快取,就從快取撈資料 + { + cachedData = await cache.GetStringAsync(key, cancel); + if (cachedData != null) + { + result = JsonSerializer.Deserialize>( + cachedData, jsonSerializerOptions); + return result; + } + } + + // await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancel); + await using var dbContext = dynamicDbContextFactory.CreateDbContext("01"); + + var selector = dbContext.Members + .Select(p => new GetMemberResponse { Id = p.Id, Name = p.Name, Age = p.Age, Email = p.Email }) + .AsNoTracking(); + + var totalCount = selector.Count(); + var paging = selector.OrderBy(p => p.Id) + .Skip(pageIndex * pageSize) + .Take(pageSize); + var data = await paging + .TagWith($"{nameof(MemberRepository)}.{nameof(this.GetMembersAsync)}") + .ToListAsync(cancel); + result = new PaginatedList(data, pageIndex, pageSize, totalCount); + cachedData = JsonSerializer.Serialize(result, jsonSerializerOptions); + cache.SetStringAsync(key, cachedData, + new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) //最好從組態設定讀取 + }, cancel); + + return result; + } + + // public async Task> + // GetMembersAsync(int pageSize, + // string nextPageToken, + // bool noCache = true, + // CancellationToken cancel = default) + // { + // // if (noCache) 永遠撈新的資料 + // // else 撈快取的資料 + // await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancel); + // var decodeResult = DecodePageToken(nextPageToken); + // var query = dbContext.Members + // .Select(p => p) + // .AsNoTracking(); + // if (decodeResult.lastSequenceId > 0) + // { + // query = query.Where(p => p.SequenceId > decodeResult.lastSequenceId); + // } + // + // query = query.Take(pageSize + 1); + // var selector = + // query.Select(p => new GetMemberResponse + // { + // Id = p.Id, + // Name = p.Name, + // Age = p.Age, + // Email = p.Email, + // SequenceId = p.SequenceId + // }); + // var results = await selector + // .TagWith($"{nameof(MemberRepository)}.{nameof(this.GetMembersAsync)}") + // .ToListAsync(cancel); + // + // // 是否有下一頁 + // var hasNextPage = results.Count > pageSize; + // + // if (hasNextPage) + // { + // // 有下一頁,刪除最後一筆 + // results.RemoveAt(results.Count - 1); + // + // // 產生下一頁的令牌 + // var after = results.LastOrDefault(); + // if (after != null) + // { + // nextPageToken = EncodePageToken(after.Id, after.SequenceId); + // } + // else + // { + // nextPageToken = null; + // } + // } + // + // return new CursorPaginatedList(results, nextPageToken, null); + // } + // + // // 將 Id 和 SequenceId 轉換為下一頁的令牌 + // public static string EncodePageToken(string? lastId, long? lastSequenceId) + // { + // if (lastId == null || lastSequenceId == null) + // { + // return null; + // } + // + // var json = JsonSerializer.Serialize(new { lastId, lastSequenceId }); + // return Convert.ToBase64String(Encoding.UTF8.GetBytes(json)); + // } + // + // // 將下一頁的令牌解碼為 Id 和 SequenceId + // private static (string lastId, long lastSequenceId) DecodePageToken(string nextToken) + // { + // if (string.IsNullOrEmpty(nextToken)) + // { + // return (null, 0); + // } + // + // string lastId = null; + // long lastSequenceId = 0; + // var base64Bytes = Convert.FromBase64String(nextToken); + // var json = Encoding.UTF8.GetString(base64Bytes); + // var jsonNode = JsonNode.Parse(json); + // var jsonObject = jsonNode.AsObject(); + // if (jsonObject.TryGetPropertyValue("lastSequenceId", out var lastSequenceIdNode)) + // { + // lastSequenceId = lastSequenceIdNode.GetValue(); + // } + // + // if (jsonObject.TryGetPropertyValue("lastId", out var lastIdNode)) + // { + // lastId = lastIdNode.GetValue(); + // } + // + // return (lastId, lastSequenceId); + // } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/PaginatedList.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/PaginatedList.cs new file mode 100644 index 00000000..d2e4cca3 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/PaginatedList.cs @@ -0,0 +1,33 @@ +namespace Lab.Sharding.WebAPI; + +enum MyEnum +{ + AA = 0, + BB +} + +public class PaginatedList +{ + public List Items { get; } + + public int PageIndex { get; } + + public int TotalPages { get; } + + public bool HasPreviousPage => PageIndex > 1; + + public bool HasNextPage => PageIndex < TotalPages; + + public PaginatedList() + { + } + + public PaginatedList(List items, int pageIndex, int pageSize, int totalCount) + { + var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize); + + Items = items; + PageIndex = pageIndex; + TotalPages = totalPages; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Program.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Program.cs new file mode 100644 index 00000000..908f90ba --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Program.cs @@ -0,0 +1,158 @@ +using Lab.Sharding.DB; +using Lab.Sharding.DB.AutoGenerated; +using Lab.Sharding.Infrastructure; +using Lab.Sharding.WebAPI; +using Lab.Sharding.WebAPI.Member; +using Scalar.AspNetCore; +using Serilog; +using Serilog.Events; + +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.File("logs/host-.txt", rollingInterval: RollingInterval.Hour) + .CreateLogger(); +Log.Information("Starting web host"); + +try +{ + if (Array.FindIndex(args, x => x == "--local") >= 0) + { + var envFolder = EnvironmentUtility.FindParentFolder("env"); + EnvironmentUtility.ReadEnvironmentFile(envFolder, "local.env"); + } + + var builder = WebApplication.CreateBuilder(args); + + // Add services to the container. + builder.Services.AddSingleton(p => JsonSerializeFactory.DefaultOptions); + builder.Services.AddControllers() + .AddJsonOptions(options => JsonSerializeFactory.Apply(options.JsonSerializerOptions)) + ; + builder.Host + .UseSerilog((context, services, config) => + config.ReadFrom.Configuration(context.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext() + .WriteTo.Console() //正式環境不要用 Console,除非有 Log Provider 專門用來收集 Console Log + .WriteTo.Seq("http://localhost:5341") //log server + .WriteTo.File("logs/aspnet-.txt", rollingInterval: RollingInterval.Minute) //正式環境不要用 File + ); + + // 確定物件都有設定 DI Container + builder.Host.UseDefaultServiceProvider(p => + { + p.ValidateScopes = true; + p.ValidateOnBuild = true; + }); + var configuration = builder.Configuration; + + builder.Services.AddStackExchangeRedisCache((options) => + { + var connectionString = configuration.GetValue(nameof(SYS_REDIS_URL)); + + // options.ConfigurationOptions = new StackExchange.Redis.ConfigurationOptions + // { + // EndPoints = { connectionString }, + // DefaultDatabase = 0, + // }; + + options.Configuration = connectionString; + + // options.InstanceName = "SampleInstance"; + }); + + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + builder.Services.AddHttpContextAccessor(); + + builder.Services.AddSingleton(_ => TimeProvider.System); + builder.Services.AddContextAccessor(); + builder.Services.AddSysEnvironments(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddExternalApiHttpClient(); + builder.Services.AddDatabase(); + builder.Services.AddScoped, DynamicDbContextFactory>(); + builder.Services.AddSingleton(p => + { + var connectionStringProvider = new ConnectionStringProvider(); + connectionStringProvider.SetConnectionStrings(new Dictionary + { + { + "DatabaseA", + "Server=localhost;Database=Member01;User Id=SA;Password=pass@w0rd1~;TrustServerCertificate=True" + }, + { + "DatabaseB", + "Server=localhost;Database=Member02;User Id=SA;Password=pass@w0rd1~;TrustServerCertificate=True" + } + }); + return connectionStringProvider; + }); + var app = builder.Build(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(options => + options.SwaggerEndpoint("/swagger/v1/swagger.yaml", + "Swagger Demo Documentation v1")); + app.UseReDoc(options => + { + options.DocumentTitle = "Swagger Demo Documentation"; + options.SpecUrl = "/swagger/v1/swagger.yaml"; + options.RoutePrefix = "redoc"; + options.ConfigObject.HideHostname = true; + }); + + app.MapScalarApiReference(p => + { + p.OpenApiRoutePattern = "/swagger/v1/swagger.json"; + + // p.EndpointPathPrefix = "scalar"; + }); + } + + app.UseAuthorization(); + app.UseMiddleware(); + app.MapDefaultControllerRoute(); + app.UseRouting(); + app.UseEndpoints(endpoints => + { + //注册Web API Controller + endpoints.MapControllers(); + + //注册MVC Controller模板 {controller=Home}/{action=Index}/{id?} + // endpoints.MapDefaultControllerRoute(); + + //注册健康检查 + // endpoints.MapHealthChecks("/_hc"); + }); + app.UseSerilogRequestLogging(); + app.UseHttpsRedirection(); + app.MapControllers(); + app.Run(); + return 0; +} +catch (Exception ex) +{ + Log.Fatal(ex, "Host terminated unexpectedly"); + return 1; +} +finally +{ + Log.CloseAndFlush(); +} + +namespace Lab.Sharding.WebAPI +{ + public partial class Program + { + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/ServiceCollectionExtension.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/ServiceCollectionExtension.cs new file mode 100644 index 00000000..91db5c0e --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/ServiceCollectionExtension.cs @@ -0,0 +1,61 @@ +using Lab.Sharding.Infrastructure.TraceContext; +using Lab.Sharding.DB; +using Lab.Sharding.DB.AutoGenerated; +using Microsoft.EntityFrameworkCore; + +namespace Lab.Sharding.WebAPI; + +public static class ServiceCollectionExtension +{ + public static IServiceCollection AddEnvironments(this IServiceCollection services) + { + services.AddSingleton(); + return services; + } + + public static IServiceCollection AddContextAccessor(this IServiceCollection services) + { + services.AddSingleton>(); + services.AddSingleton>(p => p.GetService>()); + services.AddSingleton>(p => p.GetService>()); + return services; + } + + public static void AddDatabase(this IServiceCollection services) + { + services.AddDbContextFactory((provider, builder) => + { + var environment = provider.GetService(); + var connectionString = environment.Value; + builder.UseSqlServer(connectionString) + .UseLoggerFactory(provider.GetService()) + .EnableSensitiveDataLogging() + ; + }); + } + + public static IHttpClientBuilder AddExternalApiHttpClient(this IServiceCollection services) + { + return services.AddHttpClient("externalApi", + (provider, client) => + { + var traceContext = provider.GetService(); + var externalApi = provider.GetService(); + var traceId = traceContext.TraceId; + client.BaseAddress = new Uri(externalApi.Value); + client.DefaultRequestHeaders.Add(SysHeaderNames.TraceId, traceId); + }) + .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler + { + // 改成 true,會快取 Cookie + UseCookies = false + }); + } + + public static IServiceCollection AddSysEnvironments(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + return services; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/SysHeaderNames.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/SysHeaderNames.cs new file mode 100644 index 00000000..3946d412 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/SysHeaderNames.cs @@ -0,0 +1,6 @@ +namespace Lab.Sharding.WebAPI; + +public abstract class SysHeaderNames +{ + public const string TraceId = "x-trace-id"; +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/TraceContext.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/TraceContext.cs new file mode 100644 index 00000000..6d1a897e --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/TraceContext.cs @@ -0,0 +1,14 @@ +namespace Lab.Sharding.WebAPI; + +public record TraceContext +{ + public string TraceId { get; init; } + + public string UserId { get; init; } + + public Failure SetTraceId(Failure failure) + { + failure.TraceId = this.TraceId; + return failure; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/TraceContextMiddleware.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/TraceContextMiddleware.cs new file mode 100644 index 00000000..c6aa8327 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/TraceContextMiddleware.cs @@ -0,0 +1,77 @@ +using System.Security.Claims; +using Lab.Sharding.Infrastructure.TraceContext; + +namespace Lab.Sharding.WebAPI; + +public class TraceContextMiddleware +{ + private readonly RequestDelegate _next; + + public TraceContextMiddleware(RequestDelegate next) + { + this._next = next; + } + + public async Task Invoke(HttpContext httpContext, ILogger logger) + { + var traceId = httpContext.Request.Headers[SysHeaderNames.TraceId].FirstOrDefault(); + + //// 若調用端沒有傳入 traceId,則產生一個新的 traceId + if (string.IsNullOrWhiteSpace(traceId)) + { + traceId = httpContext.TraceIdentifier; + } + + // 模擬登入 + Signin(httpContext); + + if (httpContext.User.Identity.IsAuthenticated == false) + { + httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized; + await httpContext.Response.WriteAsJsonAsync(new Failure + { + Code = nameof(FailureCode.Unauthorized), + Message = "not login", + }); + return; + } + + var userId = httpContext.User.Identity.Name; + + // 寫入 trace context 到 object context setter + var contextSetter = httpContext.RequestServices.GetService>(); + contextSetter.Set(new TraceContext + { + TraceId = traceId, + UserId = userId + }); + + // 附加 traceId 與 userId 到 log 中 + using var _ = logger.BeginScope("{Location},{TraceId},{UserId}", + "TW", traceId, userId); + + // 附加 traceId 到 response header 中 + IContextGetter? contextGetter = + httpContext.RequestServices.GetService>(); + var traceContext = contextGetter.Get(); + httpContext.Response.Headers.TryAdd(SysHeaderNames.TraceId, traceContext.TraceId); + + await this._next.Invoke(httpContext); + } + + /// + /// 假的登入 + /// + /// + private static void Signin(HttpContext context) + { + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, "yao"), + new Claim(ClaimTypes.Name, "yao"), + }; + var identity = new ClaimsIdentity(claims, "Bearer"); + var principal = new ClaimsPrincipal(identity); + context.User = principal; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/appsettings.Development.json b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/appsettings.json b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.sln b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.sln new file mode 100644 index 00000000..edd1fcdd --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.sln @@ -0,0 +1,64 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Sharding.IntegrationTest", "Lab.Sharding.IntegrationTest\Lab.Sharding.IntegrationTest.csproj", "{072B154D-149F-416C-AC1A-E009FED7706E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Sharding.Infrastructure", "Lab.Sharding.Infrastructure\Lab.Sharding.Infrastructure.csproj", "{F9C2045E-64DE-417A-BCC7-FE20B982153B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Sharding.WebAPI", "Lab.Sharding.WebAPI\Lab.Sharding.WebAPI.csproj", "{5BB4C0EB-337D-44F4-BE0A-0694CAF47890}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Sharding.Testing.Common", "Lab.Sharding.Testing.Common\Lab.Sharding.Testing.Common.csproj", "{6F3E9F7A-8956-4888-A901-80ECDF8D0780}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{1F8C259E-D601-4CCD-BE0D-C1FBB36A1F62}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infra", "Infra", "{9F8A43C9-E365-42E8-B3FA-B217B30C6295}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Sharding.DB", "Lab.Sharding.DB\Lab.Sharding.DB.csproj", "{6478BF0B-92D8-458F-B808-3552B60682EA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Sharding.Contract", "Lab.Sharding.Contract\Lab.Sharding.Contract.csproj", "{F48CF870-2039-42E2-B4A1-9B0D3F1749FF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Sharding.Test", "Lab.Sharding.Test\Lab.Sharding.Test.csproj", "{70D7FB0A-9C30-4AD5-A8EE-68B26E351FF9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {072B154D-149F-416C-AC1A-E009FED7706E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {072B154D-149F-416C-AC1A-E009FED7706E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {072B154D-149F-416C-AC1A-E009FED7706E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {072B154D-149F-416C-AC1A-E009FED7706E}.Release|Any CPU.Build.0 = Release|Any CPU + {F9C2045E-64DE-417A-BCC7-FE20B982153B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F9C2045E-64DE-417A-BCC7-FE20B982153B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F9C2045E-64DE-417A-BCC7-FE20B982153B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F9C2045E-64DE-417A-BCC7-FE20B982153B}.Release|Any CPU.Build.0 = Release|Any CPU + {5BB4C0EB-337D-44F4-BE0A-0694CAF47890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5BB4C0EB-337D-44F4-BE0A-0694CAF47890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5BB4C0EB-337D-44F4-BE0A-0694CAF47890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5BB4C0EB-337D-44F4-BE0A-0694CAF47890}.Release|Any CPU.Build.0 = Release|Any CPU + {6F3E9F7A-8956-4888-A901-80ECDF8D0780}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6F3E9F7A-8956-4888-A901-80ECDF8D0780}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F3E9F7A-8956-4888-A901-80ECDF8D0780}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6F3E9F7A-8956-4888-A901-80ECDF8D0780}.Release|Any CPU.Build.0 = Release|Any CPU + {6478BF0B-92D8-458F-B808-3552B60682EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6478BF0B-92D8-458F-B808-3552B60682EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6478BF0B-92D8-458F-B808-3552B60682EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6478BF0B-92D8-458F-B808-3552B60682EA}.Release|Any CPU.Build.0 = Release|Any CPU + {F48CF870-2039-42E2-B4A1-9B0D3F1749FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F48CF870-2039-42E2-B4A1-9B0D3F1749FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F48CF870-2039-42E2-B4A1-9B0D3F1749FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F48CF870-2039-42E2-B4A1-9B0D3F1749FF}.Release|Any CPU.Build.0 = Release|Any CPU + {70D7FB0A-9C30-4AD5-A8EE-68B26E351FF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {70D7FB0A-9C30-4AD5-A8EE-68B26E351FF9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {70D7FB0A-9C30-4AD5-A8EE-68B26E351FF9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {70D7FB0A-9C30-4AD5-A8EE-68B26E351FF9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {072B154D-149F-416C-AC1A-E009FED7706E} = {1F8C259E-D601-4CCD-BE0D-C1FBB36A1F62} + {F9C2045E-64DE-417A-BCC7-FE20B982153B} = {9F8A43C9-E365-42E8-B3FA-B217B30C6295} + {6F3E9F7A-8956-4888-A901-80ECDF8D0780} = {1F8C259E-D601-4CCD-BE0D-C1FBB36A1F62} + {6478BF0B-92D8-458F-B808-3552B60682EA} = {9F8A43C9-E365-42E8-B3FA-B217B30C6295} + {F48CF870-2039-42E2-B4A1-9B0D3F1749FF} = {9F8A43C9-E365-42E8-B3FA-B217B30C6295} + {70D7FB0A-9C30-4AD5-A8EE-68B26E351FF9} = {1F8C259E-D601-4CCD-BE0D-C1FBB36A1F62} + EndGlobalSection +EndGlobal