From 3125d647cef374f5af3e4a2ba089ef1884c52da1 Mon Sep 17 00:00:00 2001 From: salnyed Date: Sat, 14 Mar 2026 22:02:47 +0400 Subject: [PATCH 1/2] =?UTF-8?q?=D0=92=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BB=D0=B0=D0=B1=D0=BE=D1=80=D0=B0=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=BD=D0=BE=D0=B9=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 153 +++++++ .github/DISCUSSION_TEMPLATE/questions.yml | 38 ++ .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 + .../inspectionProfiles/Project_Default.xml | 427 ++++++++++++++++++ .idea/.idea.CloudDevelopment/.idea/vcs.xml | 6 + Client.Wasm/App.razor | 12 + Client.Wasm/Client.Wasm.csproj | 21 + Client.Wasm/Components/DataCard.razor | 141 ++++++ Client.Wasm/Components/StudentCard.razor | 13 + 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 | 43 ++ LICENSE | 21 + Patient.AppHost/AppHost.cs | 13 + Patient.AppHost/Patient.AppHost.csproj | 23 + .../Properties/launchSettings.json | 29 ++ Patient.AppHost/appsettings.Development.json | 8 + Patient.AppHost/appsettings.json | 9 + .../Controller/PatientController.cs | 36 ++ Patient.Generator/DTO/PatientDto.cs | 57 +++ .../Generator/PatientGenerator.cs | 113 +++++ Patient.Generator/Patient.Generator.csproj | 20 + Patient.Generator/Program.cs | 42 ++ .../Properties/LaunchSettings.json | 38 ++ Patient.Generator/Service/IPatientCache.cs | 25 + Patient.Generator/Service/IPatientService.cs | 17 + Patient.Generator/Service/PatientCache.cs | 99 ++++ Patient.Generator/Service/PatientService.cs | 32 ++ .../appsettings.Development.json | 8 + Patient.Generator/appsettings.json | 12 + Patient.ServiceDefaults/Extensions.cs | 100 ++++ .../Patient.ServiceDefaults.csproj | 22 + README.md | 147 ++++++ 48 files changed, 2565 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/DISCUSSION_TEMPLATE/questions.yml 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 .idea/.idea.CloudDevelopment/.idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/.idea.CloudDevelopment/.idea/vcs.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 Patient.AppHost/AppHost.cs create mode 100644 Patient.AppHost/Patient.AppHost.csproj create mode 100644 Patient.AppHost/Properties/launchSettings.json create mode 100644 Patient.AppHost/appsettings.Development.json create mode 100644 Patient.AppHost/appsettings.json create mode 100644 Patient.Generator/Controller/PatientController.cs create mode 100644 Patient.Generator/DTO/PatientDto.cs create mode 100644 Patient.Generator/Generator/PatientGenerator.cs create mode 100644 Patient.Generator/Patient.Generator.csproj create mode 100644 Patient.Generator/Program.cs create mode 100644 Patient.Generator/Properties/LaunchSettings.json create mode 100644 Patient.Generator/Service/IPatientCache.cs create mode 100644 Patient.Generator/Service/IPatientService.cs create mode 100644 Patient.Generator/Service/PatientCache.cs create mode 100644 Patient.Generator/Service/PatientService.cs create mode 100644 Patient.Generator/appsettings.Development.json create mode 100644 Patient.Generator/appsettings.json create mode 100644 Patient.ServiceDefaults/Extensions.cs create mode 100644 Patient.ServiceDefaults/Patient.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/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/.idea/.idea.CloudDevelopment/.idea/inspectionProfiles/Project_Default.xml b/.idea/.idea.CloudDevelopment/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..5b90977 --- /dev/null +++ b/.idea/.idea.CloudDevelopment/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,427 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.CloudDevelopment/.idea/vcs.xml b/.idea/.idea.CloudDevelopment/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/.idea.CloudDevelopment/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ 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..556f073 --- /dev/null +++ b/Client.Wasm/Components/DataCard.razor @@ -0,0 +1,141 @@ +@inject IConfiguration Configuration +@inject HttpClient Client + + + + + Характеристики пациента + + + + + + # + Характеристика + Значение + + + + @if (Value is null) + { + + 1 + нет данных + нет данных + + } + else + { + var rows = BuildRows(); + foreach (var row in rows) + { + + @row.Number + @row.Name + @row.Value + + } + } + +
+
+
+ + + + Запросить нового пациента + + + + + Идентификатор пациента: + + + + + + + + + + +
+ +@code { + private static readonly string[] PropertyOrder = + [ + "id", + "fullName", + "address", + "birthDate", + "height", + "weight", + "bloodGroup", + "rhFactor", + "lastExaminationDate", + "isVaccinated" + ]; + + private static readonly Dictionary PropertyNames = new() + { + ["id"] = "Идентификатор в системе", + ["fullName"] = "ФИО пациента", + ["address"] = "Адрес проживания", + ["birthDate"] = "Дата рождения", + ["height"] = "Рост", + ["weight"] = "Вес", + ["bloodGroup"] = "Группа крови", + ["rhFactor"] = "Резус-фактор", + ["lastExaminationDate"] = "Дата последнего осмотра", + ["isVaccinated"] = "Отметка о вакцинации" + }; + + private JsonObject? Value { get; set; } + private int Id { get; set; } + + private async Task RequestNewData() + { + var baseAddress = Configuration["BaseAddress"] ?? throw new KeyNotFoundException("Конфигурация клиента не содержит параметра BaseAddress"); + Value = await Client.GetFromJsonAsync($"{baseAddress}?id={Id}", new JsonSerializerOptions { }); + StateHasChanged(); + } + + private IReadOnlyList<(int Number, string Name, string Value)> BuildRows() + { + if (Value is null) + { + return []; + } + + var rows = new List<(int Number, string Name, string Value)>(); + + for (var i = 0; i < PropertyOrder.Length; i++) + { + var propertyKey = PropertyOrder[i]; + var displayName = PropertyNames.GetValueOrDefault(propertyKey, propertyKey); + var displayValue = FormatValue(propertyKey, Value[propertyKey]); + rows.Add((i + 1, displayName, displayValue)); + } + + return rows; + } + + private static string FormatValue(string propertyKey, JsonNode? node) + { + if (node is null) + { + return "нет данных"; + } + + var rawValue = node.ToString(); + + return propertyKey switch + { + "height" => $"{rawValue} см", + "weight" => $"{rawValue} кг", + "rhFactor" => rawValue.Equals("true", StringComparison.OrdinalIgnoreCase) ? "положительный" : "отрицательный", + "isVaccinated" => rawValue.Equals("true", StringComparison.OrdinalIgnoreCase) ? "да" : "нет", + _ => rawValue + }; + } +} diff --git a/Client.Wasm/Components/StudentCard.razor b/Client.Wasm/Components/StudentCard.razor new file mode 100644 index 0000000..4c89aab --- /dev/null +++ b/Client.Wasm/Components/StudentCard.razor @@ -0,0 +1,13 @@ + + + Вариант проекта + + + + Предметная область «Медицинский пациент» + Балансировка Weighted Round Robin + Брокер сообщений SQS + Объектное хранилище Minio + + + 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..20b4fa7 --- /dev/null +++ b/Client.Wasm/Pages/Home.razor @@ -0,0 +1,3 @@ +@page "/" + + 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..5b318db --- /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..9d27bea --- /dev/null +++ b/Client.Wasm/wwwroot/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "BaseAddress": "http://localhost:5204/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..a3e3707 --- /dev/null +++ b/CloudDevelopment.sln @@ -0,0 +1,43 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36811.4 +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}") = "Patient.ServiceDefaults", "Patient.ServiceDefaults\Patient.ServiceDefaults.csproj", "{97B30C3C-3125-4E99-BA67-240DD8126A25}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Patient.Generator", "Patient.Generator\Patient.Generator.csproj", "{A9B4BB9B-9F03-4C1F-AB67-9DAD2E4D66BA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Patient.AppHost", "Patient.AppHost\Patient.AppHost.csproj", "{07AFB6CB-7359-432D-BF0B-14BA7C582AA5}" +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 + {97B30C3C-3125-4E99-BA67-240DD8126A25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97B30C3C-3125-4E99-BA67-240DD8126A25}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97B30C3C-3125-4E99-BA67-240DD8126A25}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97B30C3C-3125-4E99-BA67-240DD8126A25}.Release|Any CPU.Build.0 = Release|Any CPU + {A9B4BB9B-9F03-4C1F-AB67-9DAD2E4D66BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9B4BB9B-9F03-4C1F-AB67-9DAD2E4D66BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9B4BB9B-9F03-4C1F-AB67-9DAD2E4D66BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A9B4BB9B-9F03-4C1F-AB67-9DAD2E4D66BA}.Release|Any CPU.Build.0 = Release|Any CPU + {07AFB6CB-7359-432D-BF0B-14BA7C582AA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07AFB6CB-7359-432D-BF0B-14BA7C582AA5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07AFB6CB-7359-432D-BF0B-14BA7C582AA5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07AFB6CB-7359-432D-BF0B-14BA7C582AA5}.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/Patient.AppHost/AppHost.cs b/Patient.AppHost/AppHost.cs new file mode 100644 index 0000000..3434c33 --- /dev/null +++ b/Patient.AppHost/AppHost.cs @@ -0,0 +1,13 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var cache = builder.AddRedis("patient-cache") + .WithRedisInsight(containerName: "patient-insight"); + +var generator = builder.AddProject("generator") + .WithReference(cache, "patient-cache") + .WaitFor(cache); + +builder.AddProject("client") + .WaitFor(generator); + +builder.Build().Run(); diff --git a/Patient.AppHost/Patient.AppHost.csproj b/Patient.AppHost/Patient.AppHost.csproj new file mode 100644 index 0000000..411c49f --- /dev/null +++ b/Patient.AppHost/Patient.AppHost.csproj @@ -0,0 +1,23 @@ + + + + + + Exe + net8.0 + enable + enable + ed7e1e47-dc98-4419-8424-85412466aa9b + + + + + + + + + + + + + diff --git a/Patient.AppHost/Properties/launchSettings.json b/Patient.AppHost/Properties/launchSettings.json new file mode 100644 index 0000000..330da1c --- /dev/null +++ b/Patient.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:17129;http://localhost:15221", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21101", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22255" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15221", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19083", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20274" + } + } + } +} \ No newline at end of file diff --git a/Patient.AppHost/appsettings.Development.json b/Patient.AppHost/appsettings.Development.json new file mode 100644 index 0000000..1b2d3ba --- /dev/null +++ b/Patient.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} \ No newline at end of file diff --git a/Patient.AppHost/appsettings.json b/Patient.AppHost/appsettings.json new file mode 100644 index 0000000..888f884 --- /dev/null +++ b/Patient.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} \ No newline at end of file diff --git a/Patient.Generator/Controller/PatientController.cs b/Patient.Generator/Controller/PatientController.cs new file mode 100644 index 0000000..7e34bbd --- /dev/null +++ b/Patient.Generator/Controller/PatientController.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Mvc; +using Patient.Generator.DTO; +using Patient.Generator.Service; + +namespace Patient.Generator.Controller; + +/// +/// API контроллер для работы с медицинскими пациентами. +/// +[ApiController] +[Route("api/patient")] +public sealed class PatientController(ILogger logger, IPatientService service) : ControllerBase +{ + /// + /// Получить пациента по идентификатору. + /// + /// Идентификатор пациента. + /// Токен отмены. + /// Данные пациента. + [HttpGet] + [ProducesResponseType(typeof(PatientDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task> Get([FromQuery] int id, CancellationToken cancellationToken) + { + if (id < 0) + { + return BadRequest(new { message = "id cannot be negative" }); + } + + logger.LogInformation("Request patient id={id}.", id); + var dto = await service.GetAsync(id, cancellationToken); + logger.LogInformation("Response patient id={id}.", id); + + return Ok(dto); + } +} diff --git a/Patient.Generator/DTO/PatientDto.cs b/Patient.Generator/DTO/PatientDto.cs new file mode 100644 index 0000000..ddac9da --- /dev/null +++ b/Patient.Generator/DTO/PatientDto.cs @@ -0,0 +1,57 @@ +namespace Patient.Generator.DTO; + +/// +/// DTO для передачи данных о медицинском пациенте. +/// +public sealed class PatientDto +{ + /// + /// Уникальный идентификатор пациента в системе. + /// + public int Id { get; set; } + + /// + /// Фамилия, имя и отчество пациента через пробел. + /// + public string FullName { get; set; } = string.Empty; + + /// + /// Адрес проживания пациента. + /// + public string Address { get; set; } = string.Empty; + + /// + /// Дата рождения пациента. + /// + public DateOnly BirthDate { get; set; } + + /// + /// Рост пациента в сантиметрах. + /// + public double Height { get; set; } + + /// + /// Вес пациента в килограммах. + /// + public double Weight { get; set; } + + /// + /// Группа крови от 1 до 4. + /// + public int BloodGroup { get; set; } + + /// + /// Резус-фактор пациента. + /// + public bool RhFactor { get; set; } + + /// + /// Дата последнего осмотра. + /// + public DateOnly LastExaminationDate { get; set; } + + /// + /// Отметка о вакцинации. + /// + public bool IsVaccinated { get; set; } +} diff --git a/Patient.Generator/Generator/PatientGenerator.cs b/Patient.Generator/Generator/PatientGenerator.cs new file mode 100644 index 0000000..5cc95cb --- /dev/null +++ b/Patient.Generator/Generator/PatientGenerator.cs @@ -0,0 +1,113 @@ +using Bogus; +using Patient.Generator.DTO; + +namespace Patient.Generator.Generator; + +/// +/// Генератор тестовых данных для медицинских пациентов. +/// +public sealed class PatientGenerator(ILogger logger) +{ + /// + /// Максимальный возраст пациента в годах. + /// + private const int MaxAgeYears = 100; + /// + /// Минимальный рост пациента в сантиметрах. + /// + private const double MinHeight = 50.0; + /// + /// Максимальный рост пациента в сантиметрах. + /// + private const double MaxHeight = 220.0; + /// + /// Минимальный вес пациента в килограммах. + /// + private const double MinWeight = 3.0; + /// + /// Максимальный вес пациента в килограммах. + /// + private const double MaxWeight = 250.0; + + /// + /// Faker для генерации тестовых данных пациентов. + /// + private static readonly Faker_faker = new Faker("ru") + .RuleFor(x => x.FullName, f => + { + var gender = f.PickRandom(); + var firstName = f.Name.FirstName(gender); + var patronymicBase = f.Name.FirstName(gender); + var patronymic = BuildPatronymic(patronymicBase, gender); + return $"{f.Name.LastName(gender)} {firstName} {patronymic}"; + }) + .RuleFor(x => x.Address, f => f.Address.FullAddress()) + .RuleFor(x => x.BirthDate, f => + { + var today = DateOnly.FromDateTime(DateTime.Today); + var earliestBirthDate = today.AddYears(-MaxAgeYears); + var totalDays = today.DayNumber - earliestBirthDate.DayNumber; + var offset = f.Random.Int(0, totalDays); + var birthDate = earliestBirthDate.AddDays(offset); + + return birthDate > today ? today : birthDate; + }) + .RuleFor(x => x.Height, + f => Math.Round(f.Random.Double(MinHeight, MaxHeight), 2, MidpointRounding.AwayFromZero)) + .RuleFor(x => x.Weight, + f => Math.Round(f.Random.Double(MinWeight, MaxWeight), 2, MidpointRounding.AwayFromZero)) + .RuleFor(x => x.BloodGroup, f => f.Random.Int(1, 4)) + .RuleFor(x => x.RhFactor, f => f.Random.Bool()) + .RuleFor(x => x.LastExaminationDate, (f, dto) => + { + var today = DateOnly.FromDateTime(DateTime.Today); + var totalDays = today.DayNumber - dto.BirthDate.DayNumber; + var offset = f.Random.Int(0, totalDays); + var examinationDate = dto.BirthDate.AddDays(offset); + return examinationDate < dto.BirthDate ? dto.BirthDate : examinationDate; + }) + .RuleFor(x => x.IsVaccinated, f => f.Random.Bool(0.8f)); + + /// + /// Генерирует случайные данные пациента с указанным идентификатором. + /// + /// Уникальный идентификатор пациента. + /// Объект PatientDto со случайно сгенерированными данными пациента. + public PatientDto Generate(int id) + { + logger.LogInformation("Generating patient for id={id}", id); + + var item = _faker.Generate(); + item.Id = id; + + logger.LogInformation("Patient generated: {@Patient}", new + { + item.Id, + item.FullName, + item.Address, + item.BirthDate, + item.Height, + item.Weight, + item.BloodGroup, + item.RhFactor, + item.LastExaminationDate, + item.IsVaccinated + }); + + return item; + } + + private static string BuildPatronymic(string baseName, Bogus.DataSets.Name.Gender gender) + { + var stem = baseName.TrimEnd('а', 'я', 'й', 'ь'); + + if (string.IsNullOrWhiteSpace(stem)) + { + stem = baseName; + } + + return gender == Bogus.DataSets.Name.Gender.Female + ? $"{stem}овна" + : $"{stem}ович"; + } +} diff --git a/Patient.Generator/Patient.Generator.csproj b/Patient.Generator/Patient.Generator.csproj new file mode 100644 index 0000000..4600dc2 --- /dev/null +++ b/Patient.Generator/Patient.Generator.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + Patient.Generator + + + + + + + + + + + + + diff --git a/Patient.Generator/Program.cs b/Patient.Generator/Program.cs new file mode 100644 index 0000000..5e75c70 --- /dev/null +++ b/Patient.Generator/Program.cs @@ -0,0 +1,42 @@ +using Patient.Generator.Generator; +using Patient.Generator.Service; +using Patient.ServiceDefaults; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); +builder.AddRedisDistributedCache("patient-cache"); + +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowLocalDev", policy => + { + policy + .AllowAnyOrigin() + .WithHeaders("Content-Type") + .WithMethods("GET"); + }); +}); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +app.UseCors("AllowLocalDev"); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.MapControllers(); +app.MapDefaultEndpoints(); + +app.Run(); diff --git a/Patient.Generator/Properties/LaunchSettings.json b/Patient.Generator/Properties/LaunchSettings.json new file mode 100644 index 0000000..cbeaf73 --- /dev/null +++ b/Patient.Generator/Properties/LaunchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:39434", + "sslPort": 44329 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5204", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7291;http://localhost:5204", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/Patient.Generator/Service/IPatientCache.cs b/Patient.Generator/Service/IPatientCache.cs new file mode 100644 index 0000000..3a9b869 --- /dev/null +++ b/Patient.Generator/Service/IPatientCache.cs @@ -0,0 +1,25 @@ +using Patient.Generator.DTO; + +namespace Patient.Generator.Service; + +/// +/// Интерфейс для кэширования медицинских пациентов. +/// +public interface IPatientCache +{ + /// + /// Получить пациента из кэша по идентификатору. + /// + /// Идентификатор пациента. + /// Токен отмены. + /// DTO пациента или null, если не найден в кэше. + public Task GetAsync(int id, CancellationToken cancellationToken = default); + + /// + /// Сохранить пациента в кэш. + /// + /// Идентификатор пациента. + /// DTO пациента для сохранения. + /// Токен отмены. + public Task SetAsync(int id, PatientDto value, CancellationToken cancellationToken = default); +} diff --git a/Patient.Generator/Service/IPatientService.cs b/Patient.Generator/Service/IPatientService.cs new file mode 100644 index 0000000..7e2b651 --- /dev/null +++ b/Patient.Generator/Service/IPatientService.cs @@ -0,0 +1,17 @@ +using Patient.Generator.DTO; + +namespace Patient.Generator.Service; + +/// +/// Интерфейс для сервиса работы с медицинскими пациентами. +/// +public interface IPatientService +{ + /// + /// Получить пациента по идентификатору. + /// + /// Идентификатор пациента. + /// Токен отмены. + /// DTO пациента. + public Task GetAsync(int id, CancellationToken cancellationToken = default); +} diff --git a/Patient.Generator/Service/PatientCache.cs b/Patient.Generator/Service/PatientCache.cs new file mode 100644 index 0000000..3e66059 --- /dev/null +++ b/Patient.Generator/Service/PatientCache.cs @@ -0,0 +1,99 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Caching.Distributed; +using Patient.Generator.DTO; + +namespace Patient.Generator.Service; + +/// +/// Реализация кэширования медицинских пациентов с использованием распределенного кэша. +/// +public sealed class PatientCache( + ILogger logger, + IDistributedCache cache, + IConfiguration configuration) : IPatientCache +{ + private const string CacheKeyPrefix = "patient:"; + private const int CacheExpirationTimeMinutesDefault = 15; + + private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web) + { + DefaultIgnoreCondition = JsonIgnoreCondition.Never + }; + + private readonly TimeSpan _cacheTtl = TimeSpan.FromMinutes( + configuration.GetValue("CacheSettings:ExpirationTimeMinutes", CacheExpirationTimeMinutesDefault)); + + /// + /// Получить пациента из кэша по идентификатору. + /// + /// Идентификатор пациента. + /// Токен отмены. + /// DTO пациента или null, если не найден в кэше. + public async Task GetAsync(int id, CancellationToken cancellationToken = default) + { + var cacheKey = $"{CacheKeyPrefix}{id}"; + + string? json; + try + { + json = await cache.GetStringAsync(cacheKey, cancellationToken); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Cache read failed for key={cacheKey}.", cacheKey); + return null; + } + + if (string.IsNullOrWhiteSpace(json)) + { + logger.LogInformation("Cache miss for key={cacheKey}.", cacheKey); + return null; + } + + try + { + var obj = JsonSerializer.Deserialize(json, _jsonOptions); + if (obj is null) + { + logger.LogWarning("Cache value for key={cacheKey} deserialized as null.", cacheKey); + return null; + } + + logger.LogInformation("Cache hit for id={id}.", obj.Id); + return obj; + } + catch (Exception ex) + { + logger.LogWarning(ex, "Cache JSON invalid for key={cacheKey}.", cacheKey); + return null; + } + } + + /// + /// Сохранить пациента в кэш. + /// + /// Идентификатор пациента. + /// DTO пациента для сохранения. + /// Токен отмены. + public async Task SetAsync(int id, PatientDto value, CancellationToken cancellationToken = default) + { + var cacheKey = $"{CacheKeyPrefix}{id}"; + + try + { + var options = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = _cacheTtl + }; + + var json = JsonSerializer.Serialize(value, _jsonOptions); + await cache.SetStringAsync(cacheKey, json, options, cancellationToken); + logger.LogInformation("Cached id={id} for ttl={ttlMinutes}m.", value.Id, _cacheTtl.TotalMinutes); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Cache write failed for id={id}.", value.Id); + } + } +} diff --git a/Patient.Generator/Service/PatientService.cs b/Patient.Generator/Service/PatientService.cs new file mode 100644 index 0000000..d1a97ea --- /dev/null +++ b/Patient.Generator/Service/PatientService.cs @@ -0,0 +1,32 @@ +using Patient.Generator.DTO; +using Patient.Generator.Generator; + +namespace Patient.Generator.Service; + +/// +/// Реализация сервиса работы с медицинскими пациентами. +/// +public sealed class PatientService( + PatientGenerator generator, + IPatientCache cache) : IPatientService +{ + /// + /// Получить пациента по идентификатору. Если пациент не найден в кэше, генерирует нового и сохраняет в кэш. + /// + /// Идентификатор пациента. + /// Токен отмены. + /// DTO пациента. + public async Task GetAsync(int id, CancellationToken cancellationToken = default) + { + var cached = await cache.GetAsync(id, cancellationToken); + if (cached is not null) + { + return cached; + } + + var generated = generator.Generate(id); + await cache.SetAsync(id, generated, cancellationToken); + + return generated; + } +} diff --git a/Patient.Generator/appsettings.Development.json b/Patient.Generator/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/Patient.Generator/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Patient.Generator/appsettings.json b/Patient.Generator/appsettings.json new file mode 100644 index 0000000..84a788e --- /dev/null +++ b/Patient.Generator/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "CacheSettings": { + "ExpirationTimeMinutes": 5 + }, + "AllowedHosts": "*" +} diff --git a/Patient.ServiceDefaults/Extensions.cs b/Patient.ServiceDefaults/Extensions.cs new file mode 100644 index 0000000..7615f58 --- /dev/null +++ b/Patient.ServiceDefaults/Extensions.cs @@ -0,0 +1,100 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Http; +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 Patient.ServiceDefaults; + +// Common cross-service wiring for Aspire: telemetry, health checks, service discovery, and resilient HttpClient. +public static class Extensions +{ + private const string ReadyPath = "/health"; + private const string AlivePath = "/alive"; + private const string LiveTag = "live"; + + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.AddDefaultHealthChecks(); + builder.ConfigureOpenTelemetry(); + builder.Services.AddServiceDiscovery(); + builder.Services.ConfigureHttpClientDefaults(static http => + { + http.AddStandardResilienceHandler(); + http.AddServiceDiscovery(); + }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(static logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(static metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation(options => + { + options.Filter = (HttpContext context) => !IsHealthRequest(context.Request.Path); + }) + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var otlpEndpoint = builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]; + if (!string.IsNullOrWhiteSpace(otlpEndpoint)) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + .AddCheck("self", () => HealthCheckResult.Healthy(), new[] { LiveTag }); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + if (app.Environment.IsDevelopment()) + { + app.MapHealthChecks(ReadyPath); + app.MapHealthChecks(AlivePath, new HealthCheckOptions + { + Predicate = r => r.Tags.Contains(LiveTag) + }); + } + + return app; + } + + private static bool IsHealthRequest(PathString path) + => path.StartsWithSegments(ReadyPath) || path.StartsWithSegments(AlivePath); +} diff --git a/Patient.ServiceDefaults/Patient.ServiceDefaults.csproj b/Patient.ServiceDefaults/Patient.ServiceDefaults.csproj new file mode 100644 index 0000000..230756f --- /dev/null +++ b/Patient.ServiceDefaults/Patient.ServiceDefaults.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0b8301c --- /dev/null +++ b/README.md @@ -0,0 +1,147 @@ +# Современные технологии разработки программного обеспечения +[Таблица с успеваемостью](https://docs.google.com/spreadsheets/d/1an43o-iqlq4V_kDtkr_y7DC221hY9qdhGPrpII27sH8/edit?usp=sharing) + +## Задание +### Цель +Реализация проекта микросервисного бекенда. + +### Задачи +* Реализация межсервисной коммуникации, +* Изучение работы с брокерами сообщений, +* Изучение архитектурных паттернов, +* Изучение работы со средствами оркестрации на примере .NET Aspire, +* Повторение основ работы с системами контроля версий, +* Интеграционное тестирование. + +### Лабораторные работы +
+1. «Кэширование» - Реализация сервиса генерации контрактов, кэширование его ответов +
+ +В рамках первой лабораторной работы необходимо: +* Реализовать сервис генерации контрактов на основе Bogus, +* Реализовать кеширование при помощи IDistributedCache и Redis, +* Реализовать структурное логирование сервиса генерации, +* Настроить оркестрацию Aspire. + +
+
+2. «Балансировка нагрузки» - Реализация апи гейтвея, настройка его работы +
+ +В рамках второй лабораторной работы необходимо: +* Настроить оркестрацию на запуск нескольких реплик сервиса генерации, +* Реализовать апи гейтвей на основе Ocelot, +* Имплементировать алгоритм балансировки нагрузки согласно варианту. + +
+
+
+3. «Интеграционное тестирование» - Реализация файлового сервиса и объектного хранилища, интеграционное тестирование бекенда +
+ +В рамках третьей лабораторной работы необходимо: +* Добавить в оркестрацию объектное хранилище, +* Реализовать файловый сервис, сериализующий сгенерированные данные в файлы и сохраняющий их в объектном хранилище, +* Реализовать отправку генерируемых данных в файловый сервис посредством брокера, +* Реализовать интеграционные тесты, проверяющие корректность работы всех сервисов бекенда вместе. + +
+
+
+4. (Опционально) «Переход на облачную инфраструктуру» - Перенос бекенда в Yandex Cloud +
+ +В рамках четвертой лабораторной работы необходимо перенестиервисы на облако все ранее разработанные сервисы: +* Клиент - в хостинг через отдельный бакет Object Storage, +* Сервис генерации - в Cloud Function, +* Апи гейтвей - в Serverless Integration как API Gateway, +* Брокер сообщений - в Message Queue, +* Файловый сервис - в Cloud Function, +* Объектное хранилище - в отдельный бакет Object Storage, + +
+
+ +## Задание. Общая часть +**Обязательно**: +* Реализация серверной части на [.NET 8](https://learn.microsoft.com/ru-ru/dotnet/core/whats-new/dotnet-8/overview). +* Оркестрация проектов при помощи [.NET Aspire](https://learn.microsoft.com/ru-ru/dotnet/aspire/get-started/aspire-overview). +* Реализация сервиса генерации данных при помощи [Bogus](https://github.com/bchavez/Bogus). +* Реализация тестов с использованием [xUnit](https://xunit.net/?tabs=cs). +* Создание минимальной документации к проекту: страница на GitHub с информацией о задании, скриншоты приложения и прочая информация. + +**Факультативно**: +* Перенос бекенда на облачную инфраструктуру Yandex Cloud + +Внимательно прочитайте [дискуссии](https://github.com/itsecd/cloud-development/discussions/1) о том, как работает автоматическое распределение на ревью. +Сразу корректно называйте свои pr, чтобы они попали на ревью нужному преподавателю. + +По итогу работы в семестре должна получиться следующая информационная система: +
+C4 диаграмма +Современные_технологии_разработки_ПО_drawio +
+ +## Варианты заданий +Номер варианта задания присваивается в начале семестра. Изменить его нельзя. Каждый вариант имеет уникальную комбинацию из предметной области, базы данных и технологии для общения сервиса генерации данных и сервера апи. + +[Список вариантов](https://docs.google.com/document/d/1WGmLYwffTTaAj4TgFCk5bUyW3XKbFMiBm-DHZrfFWr4/edit?usp=sharing) +[Список предметных областей и алгоритмов балансировки](https://docs.google.com/document/d/1PLn2lKe4swIdJDZhwBYzxqFSu0AbY2MFY1SUPkIKOM4/edit?usp=sharing) + +## Параметры этого варианта + +* Предметная область: `Медицинский пациент` +* Алгоритм балансировки: `Weighted Round Robin` +* Брокер сообщений: `SQS` +* Объектное хранилище: `Minio` + +Сервис генерации в текущем состоянии возвращает следующие характеристики пациента: + +1. Идентификатор в системе (`int`) +2. ФИО пациента (`string`) +3. Адрес проживания (`string`) +4. Дата рождения (`DateOnly`) +5. Рост (`double`) +6. Вес (`double`) +7. Группа крови (`int`) +8. Резус-фактор (`bool`) +9. Дата последнего осмотра (`DateOnly`) +10. Отметка о вакцинации (`bool`) + +## Схема сдачи + +На каждую из лабораторных работ необходимо сделать отдельный [Pull Request (PR)](https://docs.github.com/en/pull-requests). + +Общая схема: +1. Сделать форк данного репозитория +2. Выполнить задание +3. Сделать PR в данный репозиторий +4. Исправить замечания после code review +5. Получить approve + +## Критерии оценивания + +Конкурентный принцип. +Так как задания в первой лабораторной будут повторяться между студентами, то выделяются следующие показатели для оценки: +1. Скорость разработки +2. Качество разработки +3. Полнота выполнения задания + +Быстрее делаете PR - у вас преимущество. +Быстрее получаете Approve - у вас преимущество. +Выполните нечто немного выходящее за рамки проекта - у вас преимущество. +Не укладываетесь в дедлайн - получаете минимально возможный балл. + +### Шкала оценивания + +- **3 балла** за качество кода, из них: + - 2 балла - базовая оценка + - 1 балл (но не более) можно получить за выполнение любого из следующих пунктов: + - Реализация факультативного функционала + - Выполнение работы раньше других: первые 5 человек из каждой группы, которые сделали PR и получили approve, получают дополнительный балл + +## Вопросы и обратная связь по курсу + +Чтобы задать вопрос по лабораторной, воспользуйтесь [соответствующим разделом дискуссий](https://github.com/itsecd/cloud-development/discussions/categories/questions) или заведите [ишью](https://github.com/itsecd/cloud-development/issues/new). +Если у вас появились идеи/пожелания/прочие полезные мысли по преподаваемой дисциплине, их можно оставить [здесь](https://github.com/itsecd/cloud-development/discussions/categories/ideas). From fcbf8ba0059777211b621877bbd4750f38ed6add Mon Sep 17 00:00:00 2001 From: salnyed Date: Sat, 14 Mar 2026 22:29:27 +0400 Subject: [PATCH 2/2] =?UTF-8?q?=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Client.Wasm/Components/DataCard.razor | 5 +++-- Client.Wasm/Program.cs | 3 +++ Client.Wasm/_Imports.razor | 10 +++++++--- Patient.AppHost/AppHost.cs | 6 ++++-- Patient.Generator/Controller/PatientController.cs | 4 ++++ Patient.Generator/DTO/PatientDto.cs | 2 ++ Patient.Generator/Generator/PatientGenerator.cs | 2 ++ Patient.Generator/Program.cs | 3 +++ Patient.Generator/Service/IPatientCache.cs | 2 ++ Patient.Generator/Service/IPatientService.cs | 2 ++ Patient.Generator/Service/PatientCache.cs | 5 +++++ Patient.Generator/Service/PatientService.cs | 2 ++ 12 files changed, 39 insertions(+), 7 deletions(-) diff --git a/Client.Wasm/Components/DataCard.razor b/Client.Wasm/Components/DataCard.razor index 556f073..1ca27c8 100644 --- a/Client.Wasm/Components/DataCard.razor +++ b/Client.Wasm/Components/DataCard.razor @@ -1,4 +1,4 @@ -@inject IConfiguration Configuration +@inject Microsoft.Extensions.Configuration.IConfiguration Configuration @inject HttpClient Client @@ -96,7 +96,8 @@ private async Task RequestNewData() { var baseAddress = Configuration["BaseAddress"] ?? throw new KeyNotFoundException("Конфигурация клиента не содержит параметра BaseAddress"); - Value = await Client.GetFromJsonAsync($"{baseAddress}?id={Id}", new JsonSerializerOptions { }); + var requestUri = string.Concat(baseAddress, "?id=", Id); + Value = await Client.GetFromJsonAsync(requestUri, new JsonSerializerOptions()); StateHasChanged(); } diff --git a/Client.Wasm/Program.cs b/Client.Wasm/Program.cs index a182a92..cbad30c 100644 --- a/Client.Wasm/Program.cs +++ b/Client.Wasm/Program.cs @@ -1,9 +1,12 @@ +using System; +using System.Net.Http; using Blazorise; using Blazorise.Bootstrap; using Blazorise.Icons.FontAwesome; using Client.Wasm; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Microsoft.Extensions.DependencyInjection; var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("#app"); diff --git a/Client.Wasm/_Imports.razor b/Client.Wasm/_Imports.razor index 5b318db..074c4f9 100644 --- a/Client.Wasm/_Imports.razor +++ b/Client.Wasm/_Imports.razor @@ -1,14 +1,18 @@ -@using System.Net.Http +@using System +@using System.Collections.Generic +@using System.Net.Http @using System.Net.Http.Json +@using System.Text.Json +@using System.Text.Json.Nodes +@using Microsoft.AspNetCore.Components @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.Extensions.Configuration @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/Patient.AppHost/AppHost.cs b/Patient.AppHost/AppHost.cs index 3434c33..c08253a 100644 --- a/Patient.AppHost/AppHost.cs +++ b/Patient.AppHost/AppHost.cs @@ -1,13 +1,15 @@ +using Aspire.Hosting; + var builder = DistributedApplication.CreateBuilder(args); var cache = builder.AddRedis("patient-cache") .WithRedisInsight(containerName: "patient-insight"); -var generator = builder.AddProject("generator") +var generator = builder.AddProject("generator", "../Patient.Generator/Patient.Generator.csproj") .WithReference(cache, "patient-cache") .WaitFor(cache); -builder.AddProject("client") +builder.AddProject("client", "../Client.Wasm/Client.Wasm.csproj") .WaitFor(generator); builder.Build().Run(); diff --git a/Patient.Generator/Controller/PatientController.cs b/Patient.Generator/Controller/PatientController.cs index 7e34bbd..17c34a0 100644 --- a/Patient.Generator/Controller/PatientController.cs +++ b/Patient.Generator/Controller/PatientController.cs @@ -1,4 +1,8 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using Patient.Generator.DTO; using Patient.Generator.Service; diff --git a/Patient.Generator/DTO/PatientDto.cs b/Patient.Generator/DTO/PatientDto.cs index ddac9da..834914b 100644 --- a/Patient.Generator/DTO/PatientDto.cs +++ b/Patient.Generator/DTO/PatientDto.cs @@ -1,3 +1,5 @@ +using System; + namespace Patient.Generator.DTO; /// diff --git a/Patient.Generator/Generator/PatientGenerator.cs b/Patient.Generator/Generator/PatientGenerator.cs index 5cc95cb..21ed6dd 100644 --- a/Patient.Generator/Generator/PatientGenerator.cs +++ b/Patient.Generator/Generator/PatientGenerator.cs @@ -1,4 +1,6 @@ +using System; using Bogus; +using Microsoft.Extensions.Logging; using Patient.Generator.DTO; namespace Patient.Generator.Generator; diff --git a/Patient.Generator/Program.cs b/Patient.Generator/Program.cs index 5e75c70..d21e6fc 100644 --- a/Patient.Generator/Program.cs +++ b/Patient.Generator/Program.cs @@ -1,3 +1,6 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Patient.Generator.Generator; using Patient.Generator.Service; using Patient.ServiceDefaults; diff --git a/Patient.Generator/Service/IPatientCache.cs b/Patient.Generator/Service/IPatientCache.cs index 3a9b869..640b59d 100644 --- a/Patient.Generator/Service/IPatientCache.cs +++ b/Patient.Generator/Service/IPatientCache.cs @@ -1,3 +1,5 @@ +using System.Threading; +using System.Threading.Tasks; using Patient.Generator.DTO; namespace Patient.Generator.Service; diff --git a/Patient.Generator/Service/IPatientService.cs b/Patient.Generator/Service/IPatientService.cs index 7e2b651..0f40a10 100644 --- a/Patient.Generator/Service/IPatientService.cs +++ b/Patient.Generator/Service/IPatientService.cs @@ -1,3 +1,5 @@ +using System.Threading; +using System.Threading.Tasks; using Patient.Generator.DTO; namespace Patient.Generator.Service; diff --git a/Patient.Generator/Service/PatientCache.cs b/Patient.Generator/Service/PatientCache.cs index 3e66059..4a41317 100644 --- a/Patient.Generator/Service/PatientCache.cs +++ b/Patient.Generator/Service/PatientCache.cs @@ -1,6 +1,11 @@ +using System; using System.Text.Json; using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using Patient.Generator.DTO; namespace Patient.Generator.Service; diff --git a/Patient.Generator/Service/PatientService.cs b/Patient.Generator/Service/PatientService.cs index d1a97ea..f959d2a 100644 --- a/Patient.Generator/Service/PatientService.cs +++ b/Patient.Generator/Service/PatientService.cs @@ -1,3 +1,5 @@ +using System.Threading; +using System.Threading.Tasks; using Patient.Generator.DTO; using Patient.Generator.Generator;