From 4d5652ef81cc901d3a65b8f91ba77087b7862e14 Mon Sep 17 00:00:00 2001 From: K0tlyarski Date: Fri, 13 Mar 2026 12:36:02 +0400 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B0=D0=BB=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82,=20=D1=82?= =?UTF-8?q?=D0=BA=20=D0=B2=20=D0=B8=D0=B7=D0=BD=D0=B0=D1=87=D0=B0=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D0=BE=D0=B9=20=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=82?= =?UTF-8?q?=D1=83=D1=80=D0=B5=20=D0=BE=D0=BD=20=D0=BD=D0=B5=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D0=B0=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 153 +++++++ .github/DISCUSSION_TEMPLATE/questions.yml | 38 ++ ...20\276\321\200\320\275\320\276\320\271.md" | 33 ++ .github/PULL_REQUEST_TEMPLATE.md | 6 + .github/workflows/setup_pr.yml | 76 ++++ .gitignore | 418 ++++++++++++++++++ .idea/.idea.CloudDevelopment/.idea/.gitignore | 15 + .idea/.idea.CloudDevelopment/.idea/.name | 1 + .../.idea/encodings.xml | 4 + .../.idea/indexLayout.xml | 8 + Client.Wasm/App.razor | 12 + Client.Wasm/Client.Wasm.csproj | 21 + Client.Wasm/Components/DataCard.razor | 135 ++++++ Client.Wasm/Components/StudentCard.razor | 16 + Client.Wasm/Layout/MainLayout.razor | 12 + Client.Wasm/Layout/MainLayout.razor.css | 77 ++++ Client.Wasm/Pages/Home.razor | 3 + Client.Wasm/Program.cs | 17 + Client.Wasm/Properties/launchSettings.json | 41 ++ Client.Wasm/_Imports.razor | 14 + Client.Wasm/wwwroot/appsettings.json | 10 + Client.Wasm/wwwroot/css/app.css | 103 +++++ Client.Wasm/wwwroot/favicon.png | Bin 0 -> 1148 bytes Client.Wasm/wwwroot/icon-192.png | Bin 0 -> 2626 bytes Client.Wasm/wwwroot/index.html | 35 ++ CloudDevelopment.sln | 48 ++ LICENSE | 21 + .../Controllers/PatientController.cs | 25 ++ ProjectApp.Api/Program.cs | 59 +++ ProjectApp.Api/ProjectApp.Api.csproj | 23 + ProjectApp.Api/Properties/launchSettings.json | 41 ++ .../IMedicalPatientGeneratorService.cs | 11 + .../MedicalPatientGenerator.cs | 75 ++++ .../MedicalPatientGeneratorService.cs | 85 ++++ ProjectApp.Api/appsettings.Development.json | 12 + ProjectApp.Api/appsettings.json | 12 + ProjectApp.AppHost/Program.cs | 14 + ProjectApp.AppHost/ProjectApp.AppHost.csproj | 24 + .../Properties/launchSettings.json | 29 ++ .../appsettings.Development.json | 8 + ProjectApp.AppHost/appsettings.json | 9 + ProjectApp.Domain/Entities/MedicalPatient.cs | 57 +++ ProjectApp.Domain/ProjectApp.Domain.csproj | 11 + ProjectApp.ServiceDefaults/Extensions.cs | 104 +++++ .../ProjectApp.ServiceDefaults.csproj | 20 + README.md | 39 ++ 46 files changed, 1975 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/DISCUSSION_TEMPLATE/questions.yml create mode 100644 ".github/ISSUE_TEMPLATE/\320\262\320\276\320\277\321\200\320\276\321\201-\320\277\320\276-\320\273\320\260\320\261\320\276\321\200\320\260\321\202\320\276\321\200\320\275\320\276\320\271.md" create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/setup_pr.yml create mode 100644 .gitignore create mode 100644 .idea/.idea.CloudDevelopment/.idea/.gitignore create mode 100644 .idea/.idea.CloudDevelopment/.idea/.name create mode 100644 .idea/.idea.CloudDevelopment/.idea/encodings.xml create mode 100644 .idea/.idea.CloudDevelopment/.idea/indexLayout.xml create mode 100644 Client.Wasm/App.razor create mode 100644 Client.Wasm/Client.Wasm.csproj create mode 100644 Client.Wasm/Components/DataCard.razor create mode 100644 Client.Wasm/Components/StudentCard.razor create mode 100644 Client.Wasm/Layout/MainLayout.razor create mode 100644 Client.Wasm/Layout/MainLayout.razor.css create mode 100644 Client.Wasm/Pages/Home.razor create mode 100644 Client.Wasm/Program.cs create mode 100644 Client.Wasm/Properties/launchSettings.json create mode 100644 Client.Wasm/_Imports.razor create mode 100644 Client.Wasm/wwwroot/appsettings.json create mode 100644 Client.Wasm/wwwroot/css/app.css create mode 100644 Client.Wasm/wwwroot/favicon.png create mode 100644 Client.Wasm/wwwroot/icon-192.png create mode 100644 Client.Wasm/wwwroot/index.html create mode 100644 CloudDevelopment.sln create mode 100644 LICENSE create mode 100644 ProjectApp.Api/Controllers/PatientController.cs create mode 100644 ProjectApp.Api/Program.cs create mode 100644 ProjectApp.Api/ProjectApp.Api.csproj create mode 100644 ProjectApp.Api/Properties/launchSettings.json create mode 100644 ProjectApp.Api/Services/ProjectGeneratorService/IMedicalPatientGeneratorService.cs create mode 100644 ProjectApp.Api/Services/ProjectGeneratorService/MedicalPatientGenerator.cs create mode 100644 ProjectApp.Api/Services/ProjectGeneratorService/MedicalPatientGeneratorService.cs create mode 100644 ProjectApp.Api/appsettings.Development.json create mode 100644 ProjectApp.Api/appsettings.json create mode 100644 ProjectApp.AppHost/Program.cs create mode 100644 ProjectApp.AppHost/ProjectApp.AppHost.csproj create mode 100644 ProjectApp.AppHost/Properties/launchSettings.json create mode 100644 ProjectApp.AppHost/appsettings.Development.json create mode 100644 ProjectApp.AppHost/appsettings.json create mode 100644 ProjectApp.Domain/Entities/MedicalPatient.cs create mode 100644 ProjectApp.Domain/ProjectApp.Domain.csproj create mode 100644 ProjectApp.ServiceDefaults/Extensions.cs create mode 100644 ProjectApp.ServiceDefaults/ProjectApp.ServiceDefaults.csproj create mode 100644 README.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0f3bba5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,153 @@ +[*.cs] +csharp_indent_labels = one_less_than_current +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_namespace_declarations = file_scoped:error +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:error +csharp_style_prefer_tuple_swap = true:suggestion +csharp_prefer_static_local_function = true:suggestion +csharp_style_prefer_readonly_struct = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_conditional_delegate_call = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_prefer_switch_expression = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion +csharp_style_var_for_built_in_types = true:error +csharp_style_var_when_type_is_apparent = true:error +csharp_style_var_elsewhere = false:silent +csharp_space_around_binary_operators = before_and_after +[*.{cs,vb}] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = error +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = error +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = error +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:error +dotnet_style_namespace_match_folder = true:error +dotnet_style_require_accessibility_modifiers = always:error +dotnet_style_readonly_field = true:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private + +dotnet_naming_style.underscored.capitalization = camel_case +dotnet_naming_style.underscored.required_prefix = _ + +dotnet_naming_rule.private_fields_underscored.symbols = private_fields +dotnet_naming_rule.private_fields_underscored.style = underscored +dotnet_naming_rule.private_fields_underscored.severity = error + +dotnet_naming_rule.constants_must_be_uppercase.symbols = public_constants +dotnet_naming_symbols.public_constants.applicable_kinds = field +dotnet_naming_symbols.public_constants.applicable_accessibilities = * +dotnet_naming_symbols.public_constants.required_modifiers = const + +dotnet_naming_rule.constants_must_be_uppercase.style = pascal_case +dotnet_naming_style.uppercase_with_underscore_separator.capitalization = pascal_case + +dotnet_naming_rule.constants_must_be_uppercase.severity = error +dotnet_style_allow_multiple_blank_lines_experimental = true:silent +dotnet_style_allow_statement_immediately_after_block_experimental = true:silent +dotnet_code_quality_unused_parameters = all:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_event = false:silent + +# For variables +dotnet_naming_symbols.local_symbol.applicable_kinds = local +dotnet_naming_style.local_style.capitalization = camel_case +dotnet_naming_rule.variables_are_camel_case.severity = error +dotnet_naming_rule.variables_are_camel_case.symbols = local_symbol +dotnet_naming_rule.variables_are_camel_case.style = local_style \ No newline at end of file diff --git a/.github/DISCUSSION_TEMPLATE/questions.yml b/.github/DISCUSSION_TEMPLATE/questions.yml new file mode 100644 index 0000000..6c8409e --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/questions.yml @@ -0,0 +1,38 @@ +labels: [q&a] +body: + - type: input + id: fio + attributes: + label: Привет, меня зовут + description: | + Напиши свои ФИО и номер группы, чтобы тебе ответил преподаватель, который ведет у тебя пары + placeholder: | + Фамилия И.О. 651Х + validations: + required: true + + - type: dropdown + id: lab + attributes: + label: У меня вопрос по + description: | + Выбери лабораторную, которая вызвала трудности + multiple: true + options: + - 1 лабораторной работе (Кэширование) + - 2 лабораторной работе (Балансировка нагрузки) + - 3 лабораторной работе (Интеграционное тестирование) + - 4 лабораторной работе (Переход на облачную инфраструктуру) + validations: + required: true + + - type: textarea + id: details + attributes: + label: Описание проблемы + description: | + Подробно опиши проблему, с которой ты столкнулся при выполнении лабораторной + placeholder: | + Также было бы крайне полезно привести помимо текстового описания проблемы скриншоты и фрагменты кода + validations: + required: true \ No newline at end of file diff --git "a/.github/ISSUE_TEMPLATE/\320\262\320\276\320\277\321\200\320\276\321\201-\320\277\320\276-\320\273\320\260\320\261\320\276\321\200\320\260\321\202\320\276\321\200\320\275\320\276\320\271.md" "b/.github/ISSUE_TEMPLATE/\320\262\320\276\320\277\321\200\320\276\321\201-\320\277\320\276-\320\273\320\260\320\261\320\276\321\200\320\260\321\202\320\276\321\200\320\275\320\276\320\271.md" new file mode 100644 index 0000000..02038ea --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\320\262\320\276\320\277\321\200\320\276\321\201-\320\277\320\276-\320\273\320\260\320\261\320\276\321\200\320\260\321\202\320\276\321\200\320\275\320\276\320\271.md" @@ -0,0 +1,33 @@ +--- +name: Вопрос по лабораторной +about: Этот шаблон предназначен для того, чтобы студенты могли задать вопрос по лабораторной +title: Вопрос по лабораторной +labels: '' +assignees: Gwymlas, alxmcs, danlla + +--- + +**Меня зовут:** +Укажите свои ФИО + +**Я из группы:** +Укажите номер группы + +**У меня вопрос по лабе:** +Укажите номер и название лабораторной, по которой появился вопрос. + +**Мой вопрос:** +Максимально подробно опишите, что вы хотите узнать/что у вас не получается/что у вас не работает. При необходимости, добавьте примеры кода. Примеры кода должны быть оформлены с использованием md разметки, чтобы их можно было удобно воспринимать: + +```cs +public class Program +{ + public static void Main(string[] args) + { + System.Console.WriteLine("Hello, World!"); + } +} +``` + +**Дополнительная информация** +Опишите тут все, что не попадает под перечисленные ранее категории (если в том есть необходимость). \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..647b33e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,6 @@ +**ФИО:** Фамилия Имя +**Номер группы:** 651Х +**Номер лабораторной:** Х +**Номер варианта:** ХХ +**Краткое описание предметной области:** Товар на складе/учебный курс/т.д. +**Краткое описание добавленных фич:** Добавлен сервис генерации/интеграционные тесты/т.д. \ No newline at end of file diff --git a/.github/workflows/setup_pr.yml b/.github/workflows/setup_pr.yml new file mode 100644 index 0000000..40fdb46 --- /dev/null +++ b/.github/workflows/setup_pr.yml @@ -0,0 +1,76 @@ +name: Setup PR for code review + +on: + pull_request_target: + types: [opened, reopened] + +permissions: write-all + +jobs: + + assign: + runs-on: ubuntu-latest + steps: + - name: Parsing your PR title for lab number + env: + TITLE: ${{ github.event.pull_request.title }} + run: | + SUB='Лаб.' + for VAR in 1 2 3 4 + do + if (echo $TITLE | grep -iqF "$SUB$VAR" )|| (echo $TITLE | grep -iqF "$SUB $VAR"); then + echo "LABEL=Lab $VAR" >> "$GITHUB_ENV" + break + fi + done + for VAR in 6511 6512 6513 + do + if (echo $TITLE | grep -iqF "$VAR" ); then + echo "GROUP=$VAR" >> "$GITHUB_ENV" + break + fi + done + + - name: Checking your lab number + run: | + if [[ $LABEL == '' ]]; then + echo "Your PR caption is not composed correctly" + exit 1 + fi + echo Your number was parsed correctly - ${{ env.LABEL }} + + - name: Checking your group number + run: | + if [[ $GROUP == '' ]]; then + echo "Your PR caption is not composed correctly" + exit 1 + fi + echo Your group was parsed correctly - ${{ env.GROUP }} + + - name: Setting PR labels + uses: actions-ecosystem/action-add-labels@v1 + with: + labels: | + ${{ env.LABEL }} + In progress + + - name: Setting reviewer + if: env.GROUP == '6511' + uses: AveryCameronUofR/add-reviewer-gh-action@1.0.3 + with: + reviewers: "danlla" + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setting reviewer + if: env.GROUP == '6512' + uses: AveryCameronUofR/add-reviewer-gh-action@1.0.3 + with: + reviewers: "Gwymlas" + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setting reviewer + if: env.GROUP == '6513' + uses: AveryCameronUofR/add-reviewer-gh-action@1.0.3 + with: + reviewers: "alxmcs" + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ce89292 --- /dev/null +++ b/.gitignore @@ -0,0 +1,418 @@ +## 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/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates +*.env + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +[Aa][Rr][Mm]64[Ee][Cc]/ +bld/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Build results on 'Bin' directories +**/[Bb]in/* +# Uncomment if you have tasks that rely on *.refresh files to move binaries +# (https://github.com/github/gitignore/pull/3736) +#!**/[Bb]in/*.refresh + +# 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.* +*.trx + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Approval Tests result files +*.received.* + +# 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/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.idb +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +# but not Directory.Build.rsp, as it configures directory-level build defaults +!Directory.Build.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.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 + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_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 +# NuGet Symbol Packages +*.snupkg +# 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 +*.appxbundle +*.appxupload + +# 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 +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# 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 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# 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/ + +# CodeRush personal settings +**/.cr/personal + +# 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 +MSBuild_Logs/ + +# AWS SAM Build and Temporary Artifacts folder +.aws-sam + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +**/.mfractor/ + +# Local History for Visual Studio +**/.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +**/.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp diff --git a/.idea/.idea.CloudDevelopment/.idea/.gitignore b/.idea/.idea.CloudDevelopment/.idea/.gitignore new file mode 100644 index 0000000..ec9f8f6 --- /dev/null +++ b/.idea/.idea.CloudDevelopment/.idea/.gitignore @@ -0,0 +1,15 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/modules.xml +/contentModel.xml +/projectSettingsUpdater.xml +/.idea.CloudDevelopment.iml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/.idea.CloudDevelopment/.idea/.name b/.idea/.idea.CloudDevelopment/.idea/.name new file mode 100644 index 0000000..8eae80e --- /dev/null +++ b/.idea/.idea.CloudDevelopment/.idea/.name @@ -0,0 +1 @@ +CloudDevelopment \ No newline at end of file diff --git a/.idea/.idea.CloudDevelopment/.idea/encodings.xml b/.idea/.idea.CloudDevelopment/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.CloudDevelopment/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.CloudDevelopment/.idea/indexLayout.xml b/.idea/.idea.CloudDevelopment/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.CloudDevelopment/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Client.Wasm/App.razor b/Client.Wasm/App.razor new file mode 100644 index 0000000..6fd3ed1 --- /dev/null +++ b/Client.Wasm/App.razor @@ -0,0 +1,12 @@ + + + + + + + Not found + +

Sorry, there's nothing at this address.

+
+
+
diff --git a/Client.Wasm/Client.Wasm.csproj b/Client.Wasm/Client.Wasm.csproj new file mode 100644 index 0000000..0ba9f90 --- /dev/null +++ b/Client.Wasm/Client.Wasm.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/Client.Wasm/Components/DataCard.razor b/Client.Wasm/Components/DataCard.razor new file mode 100644 index 0000000..5390742 --- /dev/null +++ b/Client.Wasm/Components/DataCard.razor @@ -0,0 +1,135 @@ +@inject IConfiguration Configuration +@inject HttpClient Client + + + + + Характеристики медицинского пациента + + + + + + # + Характеристика + Значение + + + + @if(Value is null) + { + + 1 + нет данных + нет данных + + } + else + { + foreach (var property in GetDisplayRows()) + { + + @property.Number + @property.Title + @property.Value + + } + } + +
+
+
+ + + + Запросить нового пациента + + + + + Идентификатор пациента: + + + + + + + + + + +
+ +@code { + private JsonObject? Value { get; set; } + private int Id { get; set; } + + private static readonly (string Key, string Title)[] PropertyOrder = + [ + ("id", "Идентификатор в системе"), + ("fullName", "ФИО пациента"), + ("address", "Адрес проживания"), + ("birthDate", "Дата рождения"), + ("height", "Рост"), + ("weight", "Вес"), + ("bloodGroup", "Группа крови"), + ("rhFactor", "Резус-фактор"), + ("lastExaminationDate", "Дата последнего осмотра"), + ("isVaccinated", "Отметка о вакцинации") + ]; + + private async Task RequestNewData() + { + var baseAddress = Configuration["BaseAddress"] ?? throw new KeyNotFoundException("Конфигурация клиента не содержит параметра BaseAddress"); + Value = await Client.GetFromJsonAsync($"{baseAddress}?id={Id}", new JsonSerializerOptions { }); + StateHasChanged(); + } + + private IEnumerable<(int Number, string Title, string Value)> GetDisplayRows() + { + if (Value is null) + { + yield break; + } + + for (var i = 0; i < PropertyOrder.Length; i++) + { + var (key, title) = PropertyOrder[i]; + var rawValue = Value[key]; + yield return (i + 1, title, FormatValue(rawValue)); + } + } + + private static string FormatValue(JsonNode? node) + { + if (node is null) + { + return "нет данных"; + } + + if (node is JsonValue value) + { + if (value.TryGetValue(out var boolValue)) + { + return boolValue ? "Да" : "Нет"; + } + + if (value.TryGetValue(out var doubleValue)) + { + return doubleValue.ToString("0.00"); + } + + if (value.TryGetValue(out var intValue)) + { + return intValue.ToString(); + } + + if (value.TryGetValue(out var stringValue)) + { + return stringValue; + } + } + + return node.ToJsonString(); + } +} diff --git a/Client.Wasm/Components/StudentCard.razor b/Client.Wasm/Components/StudentCard.razor new file mode 100644 index 0000000..ce265e1 --- /dev/null +++ b/Client.Wasm/Components/StudentCard.razor @@ -0,0 +1,16 @@ + + + Лабораторная работа + + + + Номер №1 "Кэширование" + Вариант "Медицинский пациент" + Балансировка Weighted Random + Брокер SNS + Хранилище Minio + Выполнена Котлряским Вадимом 6512 + + + + diff --git a/Client.Wasm/Layout/MainLayout.razor b/Client.Wasm/Layout/MainLayout.razor new file mode 100644 index 0000000..d6f9080 --- /dev/null +++ b/Client.Wasm/Layout/MainLayout.razor @@ -0,0 +1,12 @@ +@inherits LayoutComponentBase +
+
+
+ +
+ +
+ @Body +
+
+
diff --git a/Client.Wasm/Layout/MainLayout.razor.css b/Client.Wasm/Layout/MainLayout.razor.css new file mode 100644 index 0000000..ecf25e5 --- /dev/null +++ b/Client.Wasm/Layout/MainLayout.razor.css @@ -0,0 +1,77 @@ +.page { + position: relative; + display: flex; + flex-direction: column; +} + +main { + flex: 1; +} + +.sidebar { + background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); +} + +.top-row { + background-color: #f7f7f7; + border-bottom: 1px solid #d6d5d5; + justify-content: flex-end; + height: 3.5rem; + display: flex; + align-items: center; +} + + .top-row ::deep a, .top-row ::deep .btn-link { + white-space: nowrap; + margin-left: 1.5rem; + text-decoration: none; + } + + .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { + text-decoration: underline; + } + + .top-row ::deep a:first-child { + overflow: hidden; + text-overflow: ellipsis; + } + +@media (max-width: 640.98px) { + .top-row { + justify-content: space-between; + } + + .top-row ::deep a, .top-row ::deep .btn-link { + margin-left: 0; + } +} + +@media (min-width: 641px) { + .page { + flex-direction: row; + } + + .sidebar { + width: 250px; + height: 100vh; + position: sticky; + top: 0; + } + + .top-row { + position: sticky; + top: 0; + z-index: 1; + } + + .top-row.auth ::deep a:first-child { + flex: 1; + text-align: right; + width: 0; + } + + .top-row, article { + padding-left: 2rem !important; + padding-right: 1.5rem !important; + } +} diff --git a/Client.Wasm/Pages/Home.razor b/Client.Wasm/Pages/Home.razor new file mode 100644 index 0000000..b22b00e --- /dev/null +++ b/Client.Wasm/Pages/Home.razor @@ -0,0 +1,3 @@ +@page "/" + + \ No newline at end of file diff --git a/Client.Wasm/Program.cs b/Client.Wasm/Program.cs new file mode 100644 index 0000000..a182a92 --- /dev/null +++ b/Client.Wasm/Program.cs @@ -0,0 +1,17 @@ +using Blazorise; +using Blazorise.Bootstrap; +using Blazorise.Icons.FontAwesome; +using Client.Wasm; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); +builder.RootComponents.Add("#app"); +builder.RootComponents.Add("head::after"); + +builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); +builder.Services.AddBlazorise(options => { options.Immediate = true; }) + .AddBootstrapProviders() + .AddFontAwesomeIcons(); + +await builder.Build().RunAsync(); diff --git a/Client.Wasm/Properties/launchSettings.json b/Client.Wasm/Properties/launchSettings.json new file mode 100644 index 0000000..0d824ea --- /dev/null +++ b/Client.Wasm/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:36545", + "sslPort": 44337 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:5127", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:7282;http://localhost:5127", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Client.Wasm/_Imports.razor b/Client.Wasm/_Imports.razor new file mode 100644 index 0000000..31e16a8 --- /dev/null +++ b/Client.Wasm/_Imports.razor @@ -0,0 +1,14 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop +@using Client.Wasm.Layout +@using Client.Wasm.Components +@using Blazorise +@using Blazorise.Components +@using System.Text.Json +@using System.Text.Json.Nodes \ No newline at end of file diff --git a/Client.Wasm/wwwroot/appsettings.json b/Client.Wasm/wwwroot/appsettings.json new file mode 100644 index 0000000..f03fa18 --- /dev/null +++ b/Client.Wasm/wwwroot/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "BaseAddress": "http://localhost:5179/api/patient" +} diff --git a/Client.Wasm/wwwroot/css/app.css b/Client.Wasm/wwwroot/css/app.css new file mode 100644 index 0000000..54a8aa3 --- /dev/null +++ b/Client.Wasm/wwwroot/css/app.css @@ -0,0 +1,103 @@ +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +h1:focus { + outline: none; +} + +a, .btn-link { + color: #0071c1; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { + box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; +} + +.content { + padding-top: 1.1rem; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid red; +} + +.validation-message { + color: red; +} + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +.loading-progress { + position: relative; + display: block; + width: 8rem; + height: 8rem; + margin: 20vh auto 1rem auto; +} + + .loading-progress circle { + fill: none; + stroke: #e0e0e0; + stroke-width: 0.6rem; + transform-origin: 50% 50%; + transform: rotate(-90deg); + } + + .loading-progress circle:last-child { + stroke: #1b6ec2; + stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; + transition: stroke-dasharray 0.05s ease-in-out; + } + +.loading-progress-text { + position: absolute; + text-align: center; + font-weight: bold; + inset: calc(20vh + 3.25rem) 0 auto 0.2rem; +} + + .loading-progress-text:after { + content: var(--blazor-load-percentage-text, "Loading"); + } + +code { + color: #c02d76; +} diff --git a/Client.Wasm/wwwroot/favicon.png b/Client.Wasm/wwwroot/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8422b59695935d180d11d5dbe99653e711097819 GIT binary patch literal 1148 zcmV-?1cUpDP)9h26h2-Cs%i*@Moc3?#6qJID|D#|3|2Hn7gTIYEkr|%Xjp);YgvFmB&0#2E2b=| zkVr)lMv9=KqwN&%obTp-$<51T%rx*NCwceh-E+=&e(oLO`@Z~7gybJ#U|^tB2Pai} zRN@5%1qsZ1e@R(XC8n~)nU1S0QdzEYlWPdUpH{wJ2Pd4V8kI3BM=)sG^IkUXF2-j{ zrPTYA6sxpQ`Q1c6mtar~gG~#;lt=s^6_OccmRd>o{*=>)KS=lM zZ!)iG|8G0-9s3VLm`bsa6e ze*TlRxAjXtm^F8V`M1%s5d@tYS>&+_ga#xKGb|!oUBx3uc@mj1%=MaH4GR0tPBG_& z9OZE;->dO@`Q)nr<%dHAsEZRKl zedN6+3+uGHejJp;Q==pskSAcRcyh@6mjm2z-uG;s%dM-u0*u##7OxI7wwyCGpS?4U zBFAr(%GBv5j$jS@@t@iI8?ZqE36I^4t+P^J9D^ELbS5KMtZ z{Qn#JnSd$15nJ$ggkF%I4yUQC+BjDF^}AtB7w348EL>7#sAsLWs}ndp8^DsAcOIL9 zTOO!!0!k2`9BLk25)NeZp7ev>I1Mn={cWI3Yhx2Q#DnAo4IphoV~R^c0x&nw*MoIV zPthX?{6{u}sMS(MxD*dmd5rU(YazQE59b|TsB5Tm)I4a!VaN@HYOR)DwH1U5y(E)z zQqQU*B%MwtRQ$%x&;1p%ANmc|PkoFJZ%<-uq%PX&C!c-7ypis=eP+FCeuv+B@h#{4 zGx1m0PjS~FJt}3mdt4c!lel`1;4W|03kcZRG+DzkTy|7-F~eDsV2Tx!73dM0H0CTh zl)F-YUkE1zEzEW(;JXc|KR5{ox%YTh{$%F$a36JP6Nb<0%#NbSh$dMYF-{ z1_x(Vx)}fs?5_|!5xBTWiiIQHG<%)*e=45Fhjw_tlnmlixq;mUdC$R8v#j( zhQ$9YR-o%i5Uc`S?6EC51!bTRK=Xkyb<18FkCKnS2;o*qlij1YA@-nRpq#OMTX&RbL<^2q@0qja!uIvI;j$6>~k@IMwD42=8$$!+R^@5o6HX(*n~v0A9xRwxP|bki~~&uFk>U z#P+PQh zyZ;-jwXKqnKbb6)@RaxQz@vm={%t~VbaZrdbaZrdbaeEeXj>~BG?&`J0XrqR#sSlO zg~N5iUk*15JibvlR1f^^1czzNKWvoJtc!Sj*G37QXbZ8LeD{Fzxgdv#Q{x}ytfZ5q z+^k#NaEp>zX_8~aSaZ`O%B9C&YLHb(mNtgGD&Kezd5S@&C=n~Uy1NWHM`t07VQP^MopUXki{2^#ryd94>UJMYW|(#4qV`kb7eD)Q=~NN zaVIRi@|TJ!Rni8J=5DOutQ#bEyMVr8*;HU|)MEKmVC+IOiDi9y)vz=rdtAUHW$yjt zrj3B7v(>exU=IrzC<+?AE=2vI;%fafM}#ShGDZx=0Nus5QHKdyb9pw&4>4XCpa-o?P(Gnco1CGX|U> z$f+_tA3+V~<{MU^A%eP!8R*-sD9y<>Jc7A(;aC5hVbs;kX9&Sa$JMG!W_BLFQa*hM zri__C@0i0U1X#?)Y=)>JpvTnY6^s;fu#I}K9u>OldV}m!Ch`d1Vs@v9 zb}w(!TvOmSzmMBa9gYvD4xocL2r0ds6%Hs>Z& z#7#o9PGHDmfG%JQq`O5~dt|MAQN@2wyJw_@``7Giyy(yyk(m8U*kk5$X1^;3$a3}N^Lp6hE5!#8l z#~NYHmKAs6IAe&A;bvM8OochRmXN>`D`{N$%#dZCRxp4-dJ?*3P}}T`tYa3?zz5BA zTu7uE#GsDpZ$~j9q=Zq!LYjLbZPXFILZK4?S)C-zE1(dC2d<7nO4-nSCbV#9E|E1MM|V<9>i4h?WX*r*ul1 z5#k6;po8z=fdMiVVz*h+iaTlz#WOYmU^SX5#97H~B32s-#4wk<1NTN#g?LrYieCu> zF7pbOLR;q2D#Q`^t%QcY06*X-jM+ei7%ZuanUTH#9Y%FBi*Z#22({_}3^=BboIsbg zR0#jJ>9QR8SnmtSS6x($?$}6$x+q)697#m${Z@G6Ujf=6iO^S}7P`q8DkH!IHd4lB zDzwxt3BHsPAcXFFY^Fj}(073>NL_$A%v2sUW(CRutd%{G`5ow?L`XYSO*Qu?x+Gzv zBtR}Y6`XF4xX7)Z04D+fH;TMapdQFFameUuHL34NN)r@aF4RO%x&NApeWGtr#mG~M z6sEIZS;Uj1HB1*0hh=O@0q1=Ia@L>-tETu-3n(op+97E z#&~2xggrl(LA|giII;RwBlX2^Q`B{_t}gxNL;iB11gEPC>v` zb4SJ;;BFOB!{chn>?cCeGDKuqI0+!skyWTn*k!WiPNBf=8rn;@y%( znhq%8fj2eAe?`A5mP;TE&iLEmQ^xV%-kmC-8mWao&EUK_^=GW-Y3z ksi~={si~={skwfB0gq6itke#r1ONa407*qoM6N<$g11Kq@c;k- literal 0 HcmV?d00001 diff --git a/Client.Wasm/wwwroot/index.html b/Client.Wasm/wwwroot/index.html new file mode 100644 index 0000000..b74ee32 --- /dev/null +++ b/Client.Wasm/wwwroot/index.html @@ -0,0 +1,35 @@ + + + + + + + Client.Wasm + + + + + + + + + + + +
+ + + + +
+
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
+ + + + diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln new file mode 100644 index 0000000..7e69b88 --- /dev/null +++ b/CloudDevelopment.sln @@ -0,0 +1,48 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35931.197 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.Wasm", "Client.Wasm\Client.Wasm.csproj", "{AE7EEA74-2FE0-136F-D797-854FD87E022A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectApp.Api", "ProjectApp.Api\ProjectApp.Api.csproj", "{E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectApp.Domain", "ProjectApp.Domain\ProjectApp.Domain.csproj", "{CC5A9873-4CC3-4B71-83AF-E4FD09F7B1AD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectApp.ServiceDefaults", "ProjectApp.ServiceDefaults\ProjectApp.ServiceDefaults.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectApp.AppHost", "ProjectApp.AppHost\ProjectApp.AppHost.csproj", "{2A5FB573-9376-4FEB-9289-A8387F435C13}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.Build.0 = Release|Any CPU + {E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Release|Any CPU.Build.0 = Release|Any CPU + {CC5A9873-4CC3-4B71-83AF-E4FD09F7B1AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC5A9873-4CC3-4B71-83AF-E4FD09F7B1AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC5A9873-4CC3-4B71-83AF-E4FD09F7B1AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC5A9873-4CC3-4B71-83AF-E4FD09F7B1AD}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU + {2A5FB573-9376-4FEB-9289-A8387F435C13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A5FB573-9376-4FEB-9289-A8387F435C13}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A5FB573-9376-4FEB-9289-A8387F435C13}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A5FB573-9376-4FEB-9289-A8387F435C13}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {90FE6B04-8381-437E-893A-FEBA1DA10AEE} + EndGlobalSection +EndGlobal diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fa20eb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 alxmcs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ProjectApp.Api/Controllers/PatientController.cs b/ProjectApp.Api/Controllers/PatientController.cs new file mode 100644 index 0000000..92e57de --- /dev/null +++ b/ProjectApp.Api/Controllers/PatientController.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Mvc; +using ProjectApp.Api.Services.ProjectGeneratorService; +using ProjectApp.Domain.Entities; + +namespace ProjectApp.Api.Controllers; + +[Route("api/[controller]")] +[ApiController] +public class PatientController( + IMedicalPatientGeneratorService generatorService, + ILogger logger) : ControllerBase +{ + /// + /// Получить медицинского пациента по ID, если не найден в кэше, сгенерировать нового + /// + [HttpGet] + public async Task> GetById([FromQuery] int id, CancellationToken cancellationToken) + { + logger.LogInformation("Received request to retrieve/generate patient {Id}", id); + + var patient = await generatorService.GetByIdAsync(id, cancellationToken); + + return Ok(patient); + } +} diff --git a/ProjectApp.Api/Program.cs b/ProjectApp.Api/Program.cs new file mode 100644 index 0000000..44b1584 --- /dev/null +++ b/ProjectApp.Api/Program.cs @@ -0,0 +1,59 @@ +using ProjectApp.Api.Services.ProjectGeneratorService; +using ProjectApp.ServiceDefaults; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +builder.AddRedisDistributedCache("cache"); + +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + policy.WithOrigins("http://localhost:5127") + .WithMethods("GET") + .WithHeaders("Content-Type"); + }); +}); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(options => +{ + options.SwaggerDoc("v1", new Microsoft.OpenApi.OpenApiInfo + { + Title = "Medical Patient Generator API" + }); + + var xmlFilename = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFilename); + if (File.Exists(xmlPath)) + { + options.IncludeXmlComments(xmlPath); + } + + var domainXmlPath = Path.Combine(AppContext.BaseDirectory, "ProjectApp.Domain.xml"); + if (File.Exists(domainXmlPath)) + { + options.IncludeXmlComments(domainXmlPath); + } +}); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +app.UseCors(); +app.MapControllers(); +app.MapDefaultEndpoints(); + +app.Run(); diff --git a/ProjectApp.Api/ProjectApp.Api.csproj b/ProjectApp.Api/ProjectApp.Api.csproj new file mode 100644 index 0000000..2aea601 --- /dev/null +++ b/ProjectApp.Api/ProjectApp.Api.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + true + $(NoWarn);1591 + + + + + + + + + + + + + + + diff --git a/ProjectApp.Api/Properties/launchSettings.json b/ProjectApp.Api/Properties/launchSettings.json new file mode 100644 index 0000000..bbe9ee6 --- /dev/null +++ b/ProjectApp.Api/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:46825", + "sslPort": 44333 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5179", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7170;http://localhost:5179", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ProjectApp.Api/Services/ProjectGeneratorService/IMedicalPatientGeneratorService.cs b/ProjectApp.Api/Services/ProjectGeneratorService/IMedicalPatientGeneratorService.cs new file mode 100644 index 0000000..94b2011 --- /dev/null +++ b/ProjectApp.Api/Services/ProjectGeneratorService/IMedicalPatientGeneratorService.cs @@ -0,0 +1,11 @@ +using ProjectApp.Domain.Entities; + +namespace ProjectApp.Api.Services.ProjectGeneratorService; + +/// +/// Сервис получения медицинского пациента +/// +public interface IMedicalPatientGeneratorService +{ + public Task GetByIdAsync(int id, CancellationToken cancellationToken = default); +} diff --git a/ProjectApp.Api/Services/ProjectGeneratorService/MedicalPatientGenerator.cs b/ProjectApp.Api/Services/ProjectGeneratorService/MedicalPatientGenerator.cs new file mode 100644 index 0000000..431e185 --- /dev/null +++ b/ProjectApp.Api/Services/ProjectGeneratorService/MedicalPatientGenerator.cs @@ -0,0 +1,75 @@ +using Bogus; +using Bogus.DataSets; +using ProjectApp.Domain.Entities; + +namespace ProjectApp.Api.Services.ProjectGeneratorService; + +/// +/// Генератор случайных медицинских пациентов с использованием Bogus +/// +public class MedicalPatientGenerator +{ + private readonly Faker _faker; + + public MedicalPatientGenerator() + { + _faker = new Faker("ru") + .RuleFor(p => p.Id, f => f.IndexFaker + 1) + .RuleFor(p => p.FullName, GenerateFullName) + .RuleFor(p => p.Address, f => f.Address.FullAddress()) + .RuleFor(p => p.BirthDate, f => f.Date.PastDateOnly(90)) + .RuleFor(p => p.Height, f => Math.Round(f.Random.Double(145.0, 205.0), 2)) + .RuleFor(p => p.Weight, f => Math.Round(f.Random.Double(45.0, 140.0), 2)) + .RuleFor(p => p.BloodGroup, f => PickWeighted(f, (1, 35), (2, 25), (3, 20), (4, 20))) + .RuleFor(p => p.RhFactor, f => PickWeighted(f, (true, 85), (false, 15))) + .RuleFor(p => p.LastExaminationDate, (f, p) => GenerateExaminationDate(f, p.BirthDate)) + .RuleFor(p => p.IsVaccinated, f => PickWeighted(f, (true, 82), (false, 18))); + } + + /// + /// Генерирует одного случайного медицинского пациента + /// + public MedicalPatient Generate() => _faker.Generate(); + + private static string GenerateFullName(Faker faker) + { + var gender = faker.PickRandom(); + var lastName = faker.Name.LastName(gender); + var firstName = faker.Name.FirstName(gender); + var patronymicBase = faker.Name.FirstName(gender); + var patronymicSuffix = gender == Name.Gender.Male ? "ович" : "овна"; + + return $"{lastName} {firstName} {patronymicBase}{patronymicSuffix}"; + } + + private static DateOnly GenerateExaminationDate(Faker faker, DateOnly birthDate) + { + var birthDateTime = birthDate.ToDateTime(TimeOnly.MinValue); + var today = DateTime.Today; + + if (birthDateTime >= today) + { + return birthDate; + } + + return DateOnly.FromDateTime(faker.Date.Between(birthDateTime, today)); + } + + private static T PickWeighted(Faker faker, params (T value, int weight)[] items) + { + var totalWeight = items.Sum(item => item.weight); + var roll = faker.Random.Int(1, totalWeight); + var currentWeight = 0; + + foreach (var item in items) + { + currentWeight += item.weight; + if (roll <= currentWeight) + { + return item.value; + } + } + + return items[^1].value; + } +} diff --git a/ProjectApp.Api/Services/ProjectGeneratorService/MedicalPatientGeneratorService.cs b/ProjectApp.Api/Services/ProjectGeneratorService/MedicalPatientGeneratorService.cs new file mode 100644 index 0000000..9e8656c --- /dev/null +++ b/ProjectApp.Api/Services/ProjectGeneratorService/MedicalPatientGeneratorService.cs @@ -0,0 +1,85 @@ +using Microsoft.Extensions.Caching.Distributed; +using ProjectApp.Domain.Entities; +using System.Text.Json; + +namespace ProjectApp.Api.Services.ProjectGeneratorService; + +/// +/// Сервис получения медицинского пациента: сначала ищет в кэше, при промахе генерирует нового и сохраняет +/// +public class MedicalPatientGeneratorService( + IDistributedCache cache, + MedicalPatientGenerator generator, + IConfiguration configuration, + ILogger logger) : IMedicalPatientGeneratorService +{ + private readonly int _expirationMinutes = configuration.GetValue("CacheSettings:ExpirationMinutes", 10); + + /// + /// Возвращает медицинского пациента по идентификатору. + /// Если пациент найден в кэше, возвращается из него; иначе генерируется, сохраняется в кэш и возвращается. + /// + public async Task GetByIdAsync(int id, CancellationToken cancellationToken = default) + { + logger.LogInformation("Attempting to retrieve medical patient {Id} from cache", id); + + var cacheKey = $"medical-patient-{id}"; + + MedicalPatient? patient = null; + try + { + var cachedData = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (!string.IsNullOrEmpty(cachedData)) + { + patient = JsonSerializer.Deserialize(cachedData); + + if (patient != null) + { + logger.LogInformation("Medical patient {Id} found in cache", id); + return patient; + } + + logger.LogWarning("Patient {Id} was found in cache but could not be deserialized. Generating a new one", id); + } + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to retrieve patient {Id} from cache (error ignored)", id); + } + + logger.LogInformation("Patient {Id} not found in cache or cache unavailable, generating a new one", id); + patient = generator.Generate(); + patient.Id = id; + + try + { + logger.LogInformation("Saving patient {Id} to cache", id); + + var cacheOptions = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_expirationMinutes) + }; + + await cache.SetStringAsync( + cacheKey, + JsonSerializer.Serialize(patient), + cacheOptions, + cancellationToken); + + logger.LogInformation( + "Medical patient generated and cached: Id={Id}, FullName={FullName}, BirthDate={BirthDate}, BloodGroup={BloodGroup}, RhFactor={RhFactor}", + patient.Id, + patient.FullName, + patient.BirthDate, + patient.BloodGroup, + patient.RhFactor); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to save patient {Id} to cache (error ignored)", id); + } + + return patient; + } +} diff --git a/ProjectApp.Api/appsettings.Development.json b/ProjectApp.Api/appsettings.Development.json new file mode 100644 index 0000000..b642d7a --- /dev/null +++ b/ProjectApp.Api/appsettings.Development.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "CacheSettings": { + "ExpirationMinutes": 10 + } +} diff --git a/ProjectApp.Api/appsettings.json b/ProjectApp.Api/appsettings.json new file mode 100644 index 0000000..b642d7a --- /dev/null +++ b/ProjectApp.Api/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "CacheSettings": { + "ExpirationMinutes": 10 + } +} diff --git a/ProjectApp.AppHost/Program.cs b/ProjectApp.AppHost/Program.cs new file mode 100644 index 0000000..9a749d0 --- /dev/null +++ b/ProjectApp.AppHost/Program.cs @@ -0,0 +1,14 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var redis = builder.AddRedis("cache") + .WithRedisCommander(); + +var api = builder.AddProject("projectapp-api") + .WithReference(redis) + .WaitFor(redis); + +builder.AddProject("client") + .WithReference(api) + .WaitFor(api); + +builder.Build().Run(); diff --git a/ProjectApp.AppHost/ProjectApp.AppHost.csproj b/ProjectApp.AppHost/ProjectApp.AppHost.csproj new file mode 100644 index 0000000..7704751 --- /dev/null +++ b/ProjectApp.AppHost/ProjectApp.AppHost.csproj @@ -0,0 +1,24 @@ + + + + + + Exe + net8.0 + enable + enable + true + b8f3eae0-771a-4f3a-8df3-ef0a21b09b55 + + + + + + + + + + + + + diff --git a/ProjectApp.AppHost/Properties/launchSettings.json b/ProjectApp.AppHost/Properties/launchSettings.json new file mode 100644 index 0000000..fa890ea --- /dev/null +++ b/ProjectApp.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17214;http://localhost:15105", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21185", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22273" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15105", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19190", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20136" + } + } + } +} diff --git a/ProjectApp.AppHost/appsettings.Development.json b/ProjectApp.AppHost/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/ProjectApp.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/ProjectApp.AppHost/appsettings.json b/ProjectApp.AppHost/appsettings.json new file mode 100644 index 0000000..31c092a --- /dev/null +++ b/ProjectApp.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/ProjectApp.Domain/Entities/MedicalPatient.cs b/ProjectApp.Domain/Entities/MedicalPatient.cs new file mode 100644 index 0000000..7b058d3 --- /dev/null +++ b/ProjectApp.Domain/Entities/MedicalPatient.cs @@ -0,0 +1,57 @@ +namespace ProjectApp.Domain.Entities; + +/// +/// Медицинский пациент +/// +public class MedicalPatient +{ + /// + /// Идентификатор в системе + /// + public required int Id { get; set; } + + /// + /// ФИО пациента + /// + public required string FullName { get; set; } + + /// + /// Адрес проживания + /// + public required string Address { get; set; } + + /// + /// Дата рождения + /// + public DateOnly BirthDate { get; set; } + + /// + /// Рост + /// + public double Height { get; set; } + + /// + /// Вес + /// + public double Weight { get; set; } + + /// + /// Группа крови + /// + public int BloodGroup { get; set; } + + /// + /// Резус-фактор + /// + public bool RhFactor { get; set; } + + /// + /// Дата последнего осмотра + /// + public DateOnly LastExaminationDate { get; set; } + + /// + /// Отметка о вакцинации + /// + public bool IsVaccinated { get; set; } +} diff --git a/ProjectApp.Domain/ProjectApp.Domain.csproj b/ProjectApp.Domain/ProjectApp.Domain.csproj new file mode 100644 index 0000000..fcfb265 --- /dev/null +++ b/ProjectApp.Domain/ProjectApp.Domain.csproj @@ -0,0 +1,11 @@ + + + + net8.0 + enable + enable + true + $(NoWarn);1591 + + + diff --git a/ProjectApp.ServiceDefaults/Extensions.cs b/ProjectApp.ServiceDefaults/Extensions.cs new file mode 100644 index 0000000..ac558a7 --- /dev/null +++ b/ProjectApp.ServiceDefaults/Extensions.cs @@ -0,0 +1,104 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace ProjectApp.ServiceDefaults; + +/// +/// Расширения для настройки сервисов Aspire (структурное логирование, телеметрия, health checks) +/// +public static class Extensions +{ + /// + /// Добавляет стандартные настройки Aspire для сервисов + /// + public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) + { + builder.ConfigureOpenTelemetry(); + builder.AddDefaultHealthChecks(); + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + http.AddStandardResilienceHandler(); + http.AddServiceDiscovery(); + }); + + return builder; + } + + /// + /// Настройка OpenTelemetry для структурного логирования и телеметрии + /// + public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + return builder; + } + + /// + /// Добавляет стандартные health checks + /// + public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) + { + builder.Services.AddHealthChecks() + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + /// + /// Настройка стандартных endpoints (health checks) + /// + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + if (app.Environment.IsDevelopment()) + { + app.MapHealthChecks("/health"); + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/ProjectApp.ServiceDefaults/ProjectApp.ServiceDefaults.csproj b/ProjectApp.ServiceDefaults/ProjectApp.ServiceDefaults.csproj new file mode 100644 index 0000000..bf9f33a --- /dev/null +++ b/ProjectApp.ServiceDefaults/ProjectApp.ServiceDefaults.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..3797f00 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# Лабораторная работа №1 — «Кэширование» + +**Вариант:** «Медицинский пациент» +**Балансировка:** Weighted Random +**Брокер:** SNS +**Хостинг S3:** Minio +**Выполнил:** Котлряский Вадим, 6512 + +## Что реализовано + +- Генерация сущности «Медицинский пациент» через Bogus. +- Кэширование результатов генерации через IDistributedCache (Redis) с TTL 10 минут. +- Структурное логирование запросов и результатов генерации. +- Оркестрация сервисов через .NET Aspire. +- REST endpoint: `GET /api/patient?id={id}`. + +## Характеристики генерируемого пациента + +1. Идентификатор в системе — `int` +2. ФИО пациента — `string` +3. Адрес проживания — `string` +4. Дата рождения — `DateOnly` +5. Рост — `double` +6. Вес — `double` +7. Группа крови — `int` +8. Резус-фактор — `bool` +9. Дата последнего осмотра — `DateOnly` +10. Отметка о вакцинации — `bool` + +## Правила генерации + +- ФИО пациента: конкатенация фамилии, имени и отчества через пробел, все значения берутся из секции `Name`. +- Адрес проживания: значение берется из секции `Address`. +- Дата рождения не может быть позже текущей даты. +- Рост и вес округляются до 2 знаков после запятой. +- Рост и вес генерируются в разумных пределах. +- Группа крови: число от 1 до 4. +- Дата последнего осмотра не может быть раньше даты рождения. +- Для части характеристик используется взвешенная случайность в соответствии с вариантом `Weighted Random`.