diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b307fd9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,297 @@ +# editorconfig.org + +# top-most EditorConfig file +root = true + +# Copyright File Header +file_header_template = SPDX-FileCopyrightText: © [year file created] - [last year file modified], MONAI Consortium\nSPDX-License-Identifier: Apache License 2.0 +dotnet_diagnostic.IDE0073.severity = error + +# Default settings: +# A newline ending every file +# Use 4 spaces as indentation +[*] +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +# Generated code +[*{_AssemblyInfo.cs,.notsupported.cs,AsmOffsets.cs}] +generated_code = true +dotnet_style_namespace_match_folder = true:warn +dotnet_style_readonly_field = true:warn +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members: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:warning +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent + +# avoid this. unless absolutely necessary +dotnet_style_qualification_for_field = false:error +dotnet_style_qualification_for_property = false:error +dotnet_style_qualification_for_method = false:error +dotnet_style_qualification_for_event = false:error + +# C# files +[*.cs] +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = false +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_switch_labels = true +csharp_indent_labels = one_less_than_current + +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion + + +# Types: use keywords instead of BCL types, and permit var only when the type is clear +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion + +# common styles +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# name mostly everything using PascalCase be default +dotnet_naming_rule.mostly_everything_should_be_pascal_case.severity = error +dotnet_naming_rule.mostly_everything_should_be_pascal_case.symbols = mostly_everything +dotnet_naming_rule.mostly_everything_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.mostly_everything.applicable_kinds = namespace,class,struct,interface,enum,property,method,field,event,delegate,type_parameter,local_function + +# interfaces should begin with an uppercase I +dotnet_naming_rule.interfaces_begin_with_i.severity = error +dotnet_naming_rule.interfaces_begin_with_i.symbols = interfaces +dotnet_naming_rule.interfaces_begin_with_i.style = i_prefix_pascal_case +dotnet_naming_symbols.interfaces.applicable_kinds = interface +dotnet_naming_style.i_prefix_pascal_case.required_prefix = I +dotnet_naming_style.i_prefix_pascal_case.capitalization = pascal_case + +# name all constant fields using PascalCase +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = error +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const + +# static readonly fields should be PascalCase +dotnet_naming_rule.static_readonly_fields_pascal_case.severity = error +dotnet_naming_rule.static_readonly_fields_pascal_case.symbols = static_readonly_fields +dotnet_naming_rule.static_readonly_fields_pascal_case.style = pascal_case_style +dotnet_naming_symbols.static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.static_readonly_fields.required_modifiers = readonly,static + +# static fields should have s_ prefix +dotnet_naming_rule.static_fields_should_have_prefix.severity = error +dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields +dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static +dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected +dotnet_naming_style.static_prefix_style.required_prefix = s_ +dotnet_naming_style.static_prefix_style.capitalization = camel_case + +# internal and private fields should be _camelCase +dotnet_naming_rule.camel_case_for_private_internal_fields.severity = error +dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields +dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style +dotnet_naming_symbols.private_internal_fields.applicable_kinds = field +dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal +dotnet_naming_style.camel_case_underscore_style.required_prefix = _ +dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case + +# Code style defaults +csharp_using_directive_placement = outside_namespace:warning +dotnet_sort_system_directives_first = true +csharp_prefer_braces = true:silent +csharp_preserve_single_line_blocks = true:none +csharp_preserve_single_line_statements = false:none +csharp_prefer_static_local_function = true:suggestion +csharp_prefer_simple_using_statement = true:none +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion + +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +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_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:error +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +csharp_prefer_simple_default_expression = true:suggestion + +# Expression-bodied members +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_constructors = true:silent +csharp_style_expression_bodied_operators = true: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 = true:silent + +# Pattern matching +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion + +# Null checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Other features +csharp_style_prefer_index_operator = true:none +csharp_style_prefer_range_operator = true:none +csharp_style_pattern_local_over_anonymous_function = true:none + + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Misc styles +csharp_style_namespace_declarations = block_scoped:silent + +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion + +dotnet_style_operator_placement_when_wrapping = beginning_of_line + + +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + + +# Analyzers +# Note that the above severities do not affect builds by design. These values are only used +# to configure the entries in Visual Studio's "Error List" and power its Intellisense. +# Instead, the rules below are used to configure build-time analyzer behavior. +# Unfortunately, some rules have been disabled due to performance reasons outside of +# Visual Studio and can be found here: +# https://github.com/dotnet/roslyn/blob/0a73f08951f408624639e1601bb828b396f154c8/src/Analyzers/Core/Analyzers/EnforceOnBuildValues.cs#L99 + +# C# Compiler Rules +dotnet_diagnostic.CS1591.severity = none # Missing XML comment for publicly visible type or member + +# Code Quality Rules +dotnet_diagnostic.CA1031.severity = none # Do not catch general exception types +dotnet_diagnostic.CA1032.severity = suggestion # Implement standard exception constructors +dotnet_diagnostic.CA1054.severity = error # URI parameters should not be strings +dotnet_diagnostic.CA1305.severity = suggestion # Specify IFormatProvider +dotnet_diagnostic.CA1716.severity = warning # Identifiers should not match keywords +dotnet_diagnostic.CA1848.severity = none # Do not encourage LoggerMessage delegates in every instance +dotnet_diagnostic.CA2007.severity = suggestion # Do not directly await a Task + +# Code Style Rules +dotnet_diagnostic.IDE0003.severity = error # Remove this or Me qualification +dotnet_diagnostic.IDE0004.severity = error # Remove unnecessary cast +dotnet_diagnostic.IDE0005.severity = error # Remove unnecessary import +dotnet_diagnostic.IDE0010.severity = none # Add missing cases to switch statement +dotnet_diagnostic.IDE0032.severity = error # Use auto property +dotnet_diagnostic.IDE0044.severity = error # Add readonly modifier +dotnet_diagnostic.IDE0055.severity = error # Fix formatting +dotnet_diagnostic.IDE0065.severity = error # 'using' directive placement +dotnet_diagnostic.IDE1005.severity = error # Use conditional delegate call +dotnet_diagnostic.IDE1006.severity = error # Naming rule violation +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_prefer_extended_property_pattern = true:suggestion + + + +# C++ Files +[*.{cpp,h,in}] +curly_bracket_next_line = true +indent_brace_style = Allman + +# Xml project files +[*.{csproj,dcproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] +indent_size = 2 + +[*.{csproj,dcproj,vbproj,proj,nativeproj,locproj}] +charset = utf-8 + +# Xml build files +[*.builds] +indent_size = 2 + +# Xml files +[*.{xml,stylecop,resx,ruleset}] +indent_size = 2 + +# Xml config files +[*.{props,targets,config,nuspec}] +indent_size = 2 + +# JSON files +[*.json] +indent_size = 2 + +# YAML files +[*.{yml,yaml}] +indent_size = 2 + +# Shell scripts +[*.sh] +end_of_line = lf + +[*.{cmd,bat}] +end_of_line = crlf + +# Python +[*.py] +max_line_length = 88 + +# JSON +[*.json] +indent_size = 2 +insert_final_newline = ignore diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..b13ab0e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,40 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +### Description +A clear and concise description of what the bug is. + +### Steps to reproduce +Please share a clear and concise description of the problem. +1. Go to '...' +2. Install '....' +3. Run commands '....' +... + +### Expected behavior +A clear and concise description of what you expected to happen. + +### Actual behavior +A clear and concise description of what actually happened. + +### Configuration + +* MONAI Deploy Messaging Library version/commit: +* OS and version (distro if applicable): +* Kubernetes version (if applicable): +* Docker version (if applicable): +* Installation source (NGC, Dockerhub, or something else): +* Hardware configuration (CPU, GPU, memory, storage, etc...): +* Application & version (e.g. Informatics Gateway, 0.1.0): + +### Regression? +Did this work in the previous build or release of the MONAI Deploy Messaging Library? If you can try a previous release or build to find out, that can help us narrow down the problem. If you don't know, that's OK. + +### Other information +(Please attach any logs available and remember to anonymize any PHI before sharing). diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..1e63bf0 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,22 @@ + + +Fixes # . + +### Description +A few sentences describing the changes proposed in this pull request. + +### Status +**Ready/Work in progress/Hold** + +### Types of changes + +- [ ] Non-breaking change (fix or new feature that would not break existing functionality). +- [ ] Breaking change (fix or new feature that would cause existing functionality to change). +- [ ] New tests added to cover the changes. +- [ ] All tests passed locally by running `./src/run-tests-in-docker.sh`. +- [ ] [Documentation comments](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/documentation-comments) included/updated. +- [ ] User guide updated. +- [ ] I have updated the changelog diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bc954a1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,286 @@ +# SPDX-FileCopyrightText: © 2022 MONAI Consortium +# SPDX-License-Identifier: Apache License 2.0 + +name: ci + +on: + # Triggers on pushes and on pull requests + push: + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +env: + BUILD_CONFIG: "Release" + SOLUTION: "Monai.Deploy.Messaging.sln" + TEST_RESULTS: "results/" + +jobs: + + analyze: + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + language: [ 'csharp' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: "6.0.x" + + - name: Enable NuGet cache + uses: actions/cache@v2.1.7 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + restore-keys: | + ${{ runner.os }}-nuget + + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + + - name: Restore dependencies + run: dotnet restore + working-directory: ./src + + - name: Build Solution + run: dotnet build -c ${{ env.BUILD_CONFIG }} --nologo ${{ env.SOLUTION }} + working-directory: ./src + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 + + - name: Secret detection + uses: zricethezav/gitleaks-action@master + + unit-test: + runs-on: ubuntu-latest + steps: + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 1.11 + + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: "6.0.x" + + - name: Enable NuGet cache + uses: actions/cache@v2.1.7 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + restore-keys: | + ${{ runner.os }}-nuget + + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Install SonarCloud scanner + if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' + run: dotnet tool install --global dotnet-sonarscanner + + - name: Restore dependencies + run: dotnet restore + working-directory: ./src + + - name: Begin SonarScanner + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: dotnet sonarscanner begin /k:"Project-MONAI_monai-deploy-messaging" /o:"project-monai" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="${{ env.TEST_RESULTS }}/**/*.xml" + working-directory: ./src + + - name: Build + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: dotnet build -c ${{ env.BUILD_CONFIG }} --nologo "${{ env.SOLUTION }}" + working-directory: ./src + + - name: Test + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: find ~+ -type f -name "*.Test.csproj" | xargs -L1 dotnet test -c ${{ env.BUILD_CONFIG }} -v=minimal -r "${{ env.TEST_RESULTS }}" --collect:"XPlat Code Coverage" --settings coverlet.runsettings + working-directory: ./src + + - name: End SonarScanner + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: dotnet sonarscanner end /d:sonar.login="${{ secrets.SONAR_TOKEN }}" + working-directory: ./src + + - uses: codecov/codecov-action@v2 + with: + token: ${{ secrets.CODECOV_TOKEN }} + directory: "src/${{ env.TEST_RESULTS }}" + files: "**/coverage.opencover.xml" + flags: unittests + name: codecov-umbrella + fail_ci_if_error: true + verbose: true + + build: + runs-on: ${{ matrix.os }} + + outputs: + majorMinorPatch: ${{ steps.gitversion.outputs.majorMinorPatch }} + + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + fail-fast: true + + permissions: + contents: write + packages: write + checks: write + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: "6.0.x" + + - name: Enable NuGet cache + uses: actions/cache@v2.1.7 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + restore-keys: | + ${{ runner.os }}-nuget + + - name: Restore dependencies + run: dotnet restore + working-directory: ./src + + - name: Install GitVersion + run: dotnet tool install --global GitVersion.Tool + + - name: Determine Version + id: gitversion + uses: gittools/actions/gitversion/execute@v0.9.11 + with: + useConfigFile: true + updateAssemblyInfo: true + updateAssemblyInfoFilename: src/AssemblyInfo.cs + + - name: Build Solution + run: dotnet build -c ${{ env.BUILD_CONFIG }} --nologo ${{ env.SOLUTION }} + working-directory: ./src + + - name: Package + env: + PACKAGEDIR: '${{ github.workspace }}/release/' + if: ${{ matrix.os == 'ubuntu-latest' }} + run: | + mkdir $PACKAGEDIR + dotnet pack --no-build -c ${{ env.BUILD_CONFIG }} -o $PACKAGEDIR -p:PackageVersion=${{ steps.gitversion.outputs.nuGetVersionV2 }} + ls -lR $PACKAGEDIR + working-directory: ./src + + - name: Upload + if: ${{ matrix.os == 'ubuntu-latest' }} + uses: actions/upload-artifact@v2.3.1 + with: + path: ${{ github.workspace }}/release/*.nupkg + retention-days: 30 + + publish: + runs-on: ubuntu-latest + needs: [build, unit-test] + steps: + - uses: actions/download-artifact@v2 + id: download + + - name: List artifacts + run: ls -ldR ${{steps.download.outputs.download-path}}/**/* + + - uses: actions/setup-dotnet@v1 + env: + NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} + with: + dotnet-version: "6.0.x" + source-url: https://nuget.pkg.github.com/Project-MONAI/index.json + + - name: Publish to GitHub + run: dotnet nuget push ${{ steps.download.outputs.download-path }}/artifact/*.nupkg + + release: + if: ${{ contains(github.ref, 'refs/heads/main') ||contains(github.head_ref, 'release/') }} + runs-on: ubuntu-latest + needs: [build, unit-test] + env: + MAJORMINORPATCH: ${{ needs.build.outputs.majorMinorPatch }} + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - uses: actions/download-artifact@v2 + id: download + + - name: List artifacts + run: ls -ldR ${{steps.download.outputs.download-path}}/**/* + + - name: Publish to NuGet.org + run: dotnet nuget push release/*.nupkg -s https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET }} --skip-duplicate + + - name: Extract owner and repo + uses: jungwinter/split@v1 + id: repo + with: + seperator: "/" + msg: ${{ github.repository }} + + - name: Install GitReleaseManager + uses: gittools/actions/gitreleasemanager/setup@v0.9.13 + with: + versionSpec: "0.13.x" + + - name: Create release with GitReleaseManager + uses: gittools/actions/gitreleasemanager/create@v0.9.13 + with: + token: ${{ secrets.GITHUB_TOKEN }} + owner: ${{ steps.repo.outputs._0 }} + repository: ${{ steps.repo.outputs._1 }} + milestone: ${{ env.MAJORMINORPATCH }} + name: Release v${{ env.MAJORMINORPATCH }} + + - name: Publish release with GitReleaseManager + uses: gittools/actions/gitreleasemanager/publish@v0.9.13 + if: ${{ contains(github.ref, 'refs/heads/main') }} + with: + token: ${{ secrets.GITHUB_TOKEN }} + owner: ${{ steps.repo.outputs._0 }} + repository: ${{ steps.repo.outputs._1 }} + tagName: ${{ env.MAJORMINORPATCH }} + + - name: Close release with GitReleaseManager + uses: gittools/actions/gitreleasemanager/close@v0.9.13 + if: ${{ contains(github.ref, 'refs/heads/main') }} + with: + token: ${{ secrets.GITHUB_TOKEN }} + owner: ${{ steps.repo.outputs._0 }} + repository: ${{ steps.repo.outputs._1 }} + milestone: ${{ env.MAJORMINORPATCH }} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..86de870 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,81 @@ + + +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at monai.contact@gmail.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..04affdb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,221 @@ + +- [Introduction](#introduction) + - [Communicate with us](#communicate-with-us) +- [The contribution process](#the-contribution-process) + - [Preparing pull requests](#preparing-pull-requests) + - [Submitting pull requests](#submitting-pull-requests) + - [Release a new version](#release-a-new-version) + + +## Introduction + +Welcome to Project MONAI Deploy Informatics Gateway! We're excited you're here and want to contribute. This documentation is intended for individuals and institutions interested in contributing to the MONAI Deploy Informatics Gateway. MONAI Deploy Informatics Gateway is an open-source project. As such, its success relies on its community of contributors willing to keep improving it. Therefore, your contribution will be a valued addition to the code base; we ask that you read this page and understand our contribution process, whether you are a seasoned open-source contributor or a first-time contributor. + +### Communicate with us + +We are happy to talk with you about your MONAI Deploy Informatics Gateway needs and your ideas for contributing to the project. One way to do this is to create an [issue](https://github.com/Project-MONAI/monai-deploy-informatics-gateway/issues/new/choose) by discussing your thoughts. It might be that a very similar feature is under development or already exists, so an issue is a great starting point. If you are looking for an issue to resolve that will help Project MONAI Deploy Informatics Gateway, see the [*good first issue*](https://github.com/Project-MONAI/monai-deploy-informatics-gateway/labels/good%20first%20issue) and [*help wanted*](https://github.com/Project-MONAI/monai-deploy-informatics-gateway/labels/help%20wanted) labels. + +## The contribution process + +_Pull request early_ + +We encourage you to create pull requests early. It helps us track the contributions under development, whether they are ready to be merged or not. Change your pull request's title to begin with `[WIP]` or [create a draft pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests) until it is ready for formal review. + +### Preparing pull requests + +This section highlights all the necessary preparation steps required before sending a pull request. +To collaborate efficiently, please read through this section and follow them. + +* [Checking the coding style](#checking-the-coding-style) +* [Test Projects](#test-projects) +* [Building documentation](#building-the-documentation) + +#### Checking the coding style + +##### C# Coding Style + +We follow the same [coding style](https://github.com/dotnet/runtime/blob/master/docs/coding-guidelines/coding-style.md) as described by [dotnet](https://github.com/dotnet)/[runtime](https://github.com/dotnet/runtime) project. + + +The general rule we follow is "use Visual Studio defaults" or simply to [CodeMaid](https://marketplace.visualstudio.com/items?itemName=SteveCadwallader.CodeMaid) extension. + +1. We use [Allman style](http://en.wikipedia.org/wiki/Indent_style#Allman_style) braces, where each brace begins on a new line. A single line statement block can go without braces, but the block must be properly indented on its line and must not be nested in other statement blocks that use braces (See rule 17 for more details). One exception is that a `using` statement is permitted to be nested within another `using` statement by starting on the following line at the same indentation level, even if the nested `using` contains a controlled block. +2. We use four spaces of indentation (no tabs). +3. We use `_camelCase` for internal and private fields and use `readonly` where possible. Prefix internal and private instance fields with `_`, static fields with `s_`, and thread static fields with `t_`. When used on static fields, `readonly` should come after `static` (e.g. `static readonly` not `readonly static`). Public fields should be used sparingly and use PascalCasing with no prefix when used. +4. We avoid `this.` unless necessary. +5. We always specify the visibility, even if it's the default (e.g. + `private string _foo' not `string _foo'). Visibility should be the first modifier (e.g. + `public abstract` not `abstract public`). +6. Namespace imports should be specified at the top of the file, *outside* of `namespace` declarations should be sorted alphabetically. +7. Avoid more than one empty line at any time. For example, do not have two + blank lines between members of a type. +8. Avoid spurious-free spaces. + For example, avoid `if (someVar == 0)...`, where the dots mark the spurious-free spaces. + Consider enabling "View White Space (Ctrl+R, Ctrl+W)" or "Edit -> Advanced -> View White Space" if using Visual Studio to aid detection. +9. If a file happens to differ in style from these guidelines (e.g. private members are named `m_member` + rather than `_member`), the existing style in that file takes precedence. +10. We only use `var` when it's obvious what the variable type is (e.g. `var stream = new FileStream(...)` not `var stream = OpenStandardInput()`). +11. We use language keywords instead of BCL types (e.g. `int, string, float` instead of `Int32, String, Single`, etc) for both type references as well as method calls (e.g. `int.Parse` instead of `Int32.Parse`). See issue [#13976](https://github.com/dotnet/runtime/issues/13976) for examples. +12. We use PascalCasing to name all our constant local variables and fields. The only exception is for interop code where the constant value should exactly match the name and value of the code you are calling via interop. +13. We use "`nameof(...)` "instead of ``` "..." ``` whenever possible and relevant. +14. Fields should be specified at the top within type declarations. +15. When including non-ASCII characters in the source code, use Unicode escape sequences (\uXXXX) instead of literal characters. Literal non-ASCII characters occasionally get garbled by a tool or editor. +16. When using labels (for goto), indent the label one less than the current indentation. +17. When using a single-statement if, we follow these conventions: + - Never use single-line form (for example: `if (source is null) throw new ArgumentNullException("source");`) + - Using braces is always accepted and required if any block of an `if`/`else if`/.../`else` compound statement uses braces or if a single statement body spans multiple lines. + - Braces may be omitted only if the body of *every* block associated with an `if`/`else if`/.../`else` compound statement is placed on a single line. + +An [EditorConfig](https://editorconfig.org "EditorConfig homepage") file (`.editorconfig`) has been provided at the root of the runtime repository, enabling C# auto-formatting conforming to the above guidelines. + + +##### License information + +All source code files should start with this paragraph: + +``` +// Copyright MONAI Consortium +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +``` + +#### Test Projects + +All C# projects reside in their directory, including a `Tests/` subdirectory. +Test projects are also linked in the main solution file [Monai.Deploy.InformaticsGateway.sln](src/Monai.Deploy.InformaticsGateway.sln) and can be executed either using Visual Studio's *Test Explorer* or with the `dotnet test` command line. + + +_If it's not tested, it's broken_ + +An appropriate set of tests should accompany all new functionality. +MONAI Deploy Informatics Gateway functionality has plenty of unit tests from which you can draw inspiration, and you can reach out to us if you are unsure how to proceed with testing. + + +#### Building the documentation + +Documentation for MONAI Deploy Informatics Gateway is located at `docs/` and requires [DocFX](https://dotnet.github.io/docfx/) to build. + +Please follow the [instructions](https://dotnet.github.io/docfx/tutorial/docfx_getting_started.html#2-use-docfx-as-a-command-line-tool) to install Mono and download the DocFX command-line tool to build the documentation. + +```bash +[path-to]/docfx.exe docs/docfx.json +``` + +#### Automatic code formatting + +Install [CodeMaid](https://marketplace.visualstudio.com/items?itemName=SteveCadwallader.CodeMaid) extension for Visual Studio or +**SHIFT+ALT+F** in Visual Studio Code. + +#### Signing your work + +MONAI enforces the [Developer Certificate of Origin](https://developercertificate.org/) (DCO) on all pull requests. +All commit messages should contain the `Signed-off-by` line with an email address. The [GitHub DCO app](https://github.com/apps/dco) is deployed on MONAI. The pull request's status will be `failed` if commits do not contain a valid `Signed-off-by` line. + +Git has a `-s' (or `--signoff`) command-line option to append this automatically to your commit message: + +```bash +git commit -s -m 'a new commit' +``` + +The commit message will be: + +```text + a new commit + + Signed-off-by: Your Name +``` + +Full text of the DCO: + +```text +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. +1 Letterman Drive +Suite D4700 +San Francisco, CA, 94129 + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. +``` + +### Submitting pull requests + +#### Branching + +- `main`: the `main` branch is **always ready** with only completed, tested, and verified features. The CI automatically triggers an official build when a PR is merged into the branch. +- `develop`: the `develop` branch is the active development branch and is for features that are ready for testing. Releases made in this branch are prefixed with `beta`. +- `release/`: `release` branches are created when a new official release is imminent and does not accept new features except bug fixes. Releases made in this branch are prefixed with `rc`. A pull request shall be created targeting `main` and `develop`. +- `feature/`: `feature` branches are created for a specific branch. Releases made in this branch are prefixed with `alpha.{branchName}`. A pull request shall be created when the feature is ready, targeting `develop` branch. + +#### Begin with Your Contribution Journey with a Pull Request + +All code changes must be done via [pull requests](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/proposing-changes-to-your-work-with-pull-requests). + +1. Create a new ticket or take a known ticket from [the issue list][issue list]. +1. Check if there's already a branch dedicated to the task. +1. If the task has not been taken, [create a new branch in your fork](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork) of the codebase named `[ticket_id]-[task_name]`. +For example, branch name `{username}/19-ci-pipeline-setup` corresponds to issue #19. +1. Ideally, the new branch should be based on the latest `develop` branch. +1. Make changes to the branch ([use detailed commit messages if possible](https://chris.beams.io/posts/git-commit/)). +1. Make sure that new tests cover the changes and the changed codebase [passes all tests locally](#test-projects). + + +##### When You Are Ready to Merge +1. [Create a new pull request](https://help.github.com/en/desktop/contributing-to-projects/creating-a-pull-request) from the task branch to the `develop` branch, with detailed descriptions of the purpose of this pull request by filling out the [template](./.github/pull_request_template.md). +1. Make sure all checks are successful +1. Complete tasks listed in the template as much as possible +1. Wait for reviews; if there are reviews, make point-to-point responses, make further code changes if needed. +1. If there are conflicts between the pull request branch and the target branch, pull the changes from the target branch and resolve the conflicts locally. +1. Reviewer and contributor may have discussions back and forth until all comments are addressed. +1. Wait for the pull request to be merged. + + + +### Release a new version + +A PR is made from a `release/` branch to the `main` branch when a new official release is ready. The CI process validates & builds all components required, composes the release notes, and publishes the build in the [Releases](https://github.com/Project-MONAI/monai-deploy-informatics-gateway/releases) section and any Docker images in the [Packages](https://github.com/orgs/Project-MONAI/packages?repo_name=monai-deploy-informatics-gateway) section. + +- [Actions](https://github.com/Project-MONAI/monai-deploy-informatics-gateway/actions) +- [Issues](https://github.com/Project-MONAI/monai-deploy-informatics-gateway/issues) +- [Milestones](https://github.com/Project-MONAI/monai-deploy-informatics-gateway/milestones) +- [Releases](https://github.com/Project-MONAI/monai-deploy-informatics-gateway/releases) +- [Packages](https://github.com/orgs/Project-MONAI/packages?repo_name=monai-deploy-informatics-gateway) diff --git a/GitReleaseManager.yaml b/GitReleaseManager.yaml new file mode 100644 index 0000000..1e70323 --- /dev/null +++ b/GitReleaseManager.yaml @@ -0,0 +1,49 @@ +# SPDX-FileCopyrightText: © 2022 MONAI Consortium +# SPDX-License-Identifier: Apache License 2.0 + +issue-labels-include: + - breaking + - feature + - enhancement + - bug + - documentation + - security +issue-labels-exclude: + - build + - refactor + - testing +issue-labels-alias: + - name: breaking + header: Breaking Change + plural: Breaking Changes + - name: feature + header: Feature + plural: Features + - name: enhancement + header: Enhancement + plural: Enhancements + - name: bug + header: Bug + plural: Bugs + - name: documentation + header: Documentation + plural: Documentation + - name: security + header: Security + plural: Security +create: + include-sha-section: true + sha-section-heading: "SHA256 Hashes of the release artifacts" + sha-section-line-format: "- `{1}\t{0}`" + allow-update-to-published: false +export: + include-created-date-in-title: true + created-date-string-format: MMMM dd, yyyy + perform-regex-removal: false +close: + use-issue-comments: true + issue-comment: |- + :tada: This issue has been resolved in version {milestone} :tada: + + The release is available on: + - [GitHub Release](https://github.com/{owner}/{repository}/releases/tag/{milestone}) diff --git a/GitVersion.yml b/GitVersion.yml new file mode 100644 index 0000000..ab2a9a2 --- /dev/null +++ b/GitVersion.yml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: © 2022 MONAI Consortium +# SPDX-License-Identifier: Apache License 2.0 + +assembly-versioning-scheme: MajorMinorPatchTag +mode: ContinuousDelivery +branches: + main: + tag: '' + release: + tag: rc + feature: + tag: alpha.{BranchName} + pull-request: + tag: pr + +ignore: + sha: [] +merge-message-formats: {} +next-version: 0.1.0 diff --git a/NuGetDefense.json b/NuGetDefense.json new file mode 100644 index 0000000..000dfd8 --- /dev/null +++ b/NuGetDefense.json @@ -0,0 +1,47 @@ +{ + "WarnOnly": false, + "VulnerabilityReports": { + "OutputTextReport": true + }, + "CheckTransitiveDependencies": true, + "CheckReferencedProjects": false, + "ErrorSettings": { + "ErrorSeverityThreshold": "any", + "Cvss3Threshold": -1, + "IgnoredPackages": [ + { + "Id": "NugetDefense" + } + ], + "IgnoredCvEs": [ + + ], + "AllowedPackages": [ + + ], + "WhiteListedPackages": [ + + ], + "BlockedPackages": [ + + ], + "BlacklistedPackages": [ + + ] + }, + "GitHubAdvisoryDatabase": { + "ApiToken": "", + "Username": "", + "Enabled": false, + "BreakIfCannotRun": false + }, + "NVD": { + "SelfUpdate": false, + "TimeoutInSeconds": 15, + "Enabled": true, + "BreakIfCannotRun": true + }, + "SensitivePackages": [ + + ] +} \ No newline at end of file diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..5e4be7e --- /dev/null +++ b/codecov.yml @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: © 2021-2022 MONAI Consortium +# SPDX-License-Identifier: Apache License 2.0 + +codecov: + require_ci_to_pass: yes + +coverage: + precision: 5 + round: down + range: "70...100" + status: + project: + default: + target: auto + threshold: 1% + base: auto + if_not_found: success + informational: false + only_pulls: false + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: + layout: "reach,diff,flags,files,footer" + behavior: default + require_changes: false + require_base: no + require_head: yes + +fixes: + - "/home/runner/work/Informatics-Gateway/Informatics-Gateway/src/::src/" diff --git a/global.json b/global.json new file mode 100644 index 0000000..d6c2c37 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "6.0.100", + "rollForward": "latestFeature" + } +} \ No newline at end of file diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..4d736c1 --- /dev/null +++ b/nuget.config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/.sonarlint/Monai.Deploy.Messaging.slconfig b/src/.sonarlint/Monai.Deploy.Messaging.slconfig new file mode 100644 index 0000000..3019488 --- /dev/null +++ b/src/.sonarlint/Monai.Deploy.Messaging.slconfig @@ -0,0 +1,15 @@ +{ + "ServerUri": "https://sonarcloud.io/", + "Organization": { + "Key": "project-monai", + "Name": "Project MONAI" + }, + "ProjectKey": "Project-MONAI_monai-deploy-messaging", + "ProjectName": "monai-deploy-messaging", + "Profiles": { + "CSharp": { + "ProfileKey": "AX96ONQSnTk2GEVJ9ILJ", + "ProfileTimestamp": "2022-03-31T10:15:41Z" + } + } +} \ No newline at end of file diff --git a/src/.sonarlint/project-monai_monai-deploy-messaging/CSharp/SonarLint.xml b/src/.sonarlint/project-monai_monai-deploy-messaging/CSharp/SonarLint.xml new file mode 100644 index 0000000..90bc98d --- /dev/null +++ b/src/.sonarlint/project-monai_monai-deploy-messaging/CSharp/SonarLint.xml @@ -0,0 +1,89 @@ + + + + + sonar.cs.analyzeGeneratedCode + false + + + sonar.cs.file.suffixes + .cs + + + sonar.cs.ignoreHeaderComments + true + + + sonar.cs.roslyn.ignoreIssues + false + + + + + S107 + + + max + 7 + + + + + S110 + + + max + 5 + + + + + S1479 + + + maximum + 30 + + + + + S2342 + + + flagsAttributeFormat + ^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$ + + + format + ^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$ + + + + + S2436 + + + max + 2 + + + maxMethod + 3 + + + + + S3776 + + + propertyThreshold + 3 + + + threshold + 15 + + + + + \ No newline at end of file diff --git a/src/.sonarlint/project-monai_monai-deploy-messagingcsharp.ruleset b/src/.sonarlint/project-monai_monai-deploy-messagingcsharp.ruleset new file mode 100644 index 0000000..f5f0145 --- /dev/null +++ b/src/.sonarlint/project-monai_monai-deploy-messagingcsharp.ruleset @@ -0,0 +1,370 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/AssemblyInfo.cs b/src/AssemblyInfo.cs new file mode 100644 index 0000000..f6fa0f8 --- /dev/null +++ b/src/AssemblyInfo.cs @@ -0,0 +1,13 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by GitVersion. +// +// You can modify this code as we will not overwrite it when re-executing GitVersion +// +//------------------------------------------------------------------------------ + +using System.Reflection; + +[assembly: AssemblyFileVersion("0.1.0.0")] +[assembly: AssemblyVersion("0.1.0.0")] +[assembly: AssemblyInformationalVersion("0.1.0+0.Branch.main.Sha.ae43f4c7111e09d2a34d881c6704a2dea81d9155")] diff --git a/src/Messaging/API/IMessageBrokerPublisherService.cs b/src/Messaging/API/IMessageBrokerPublisherService.cs new file mode 100644 index 0000000..73bfb17 --- /dev/null +++ b/src/Messaging/API/IMessageBrokerPublisherService.cs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: © 2021-2022 MONAI Consortium +// SPDX-License-Identifier: Apache License 2.0 + +using Monai.Deploy.Messaging.Messages; + +namespace Monai.Deploy.Messaging +{ + public interface IMessageBrokerPublisherService + { + /// + /// Gets or sets the name of the storage service. + /// + string Name { get; } + + /// + /// Publishes a message to the service. + /// + /// Topic where the message is published to + /// Message to be published + /// + Task Publish(string topic, Message message); + } +} diff --git a/src/Messaging/API/IMessageBrokerSubscriberService.cs b/src/Messaging/API/IMessageBrokerSubscriberService.cs new file mode 100644 index 0000000..6d14844 --- /dev/null +++ b/src/Messaging/API/IMessageBrokerSubscriberService.cs @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: © 2021-2022 MONAI Consortium +// SPDX-License-Identifier: Apache License 2.0 + +using Monai.Deploy.Messaging.Common; +using Monai.Deploy.Messaging.Messages; + +namespace Monai.Deploy.Messaging +{ + public interface IMessageBrokerSubscriberService + { + /// + /// Gets or sets the name of the storage service. + /// + string Name { get; } + + /// + /// Subscribe to a message topic & queue. + /// Either provide a topic, a queue or both. + /// + /// Name of the topic to subscribe to + /// Name of the queue to consume + /// Action to be performed when message is received + /// Number of unacknowledged messages to receive at once. Defaults to 0. + void Subscribe(string topic, string queue, Action messageReceivedCallback, ushort prefetchCount = 0); + + /// + /// Acknowledge receiving of a message with the given token. + /// + /// Message to be acknowledged. + void Acknowledge(MessageBase message); + + /// + /// Rejects a messags. + /// + /// Message to be rejected. + void Reject(MessageBase message); + } +} diff --git a/src/Messaging/AssemblyInfo.cs b/src/Messaging/AssemblyInfo.cs new file mode 100644 index 0000000..f6fa0f8 --- /dev/null +++ b/src/Messaging/AssemblyInfo.cs @@ -0,0 +1,13 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by GitVersion. +// +// You can modify this code as we will not overwrite it when re-executing GitVersion +// +//------------------------------------------------------------------------------ + +using System.Reflection; + +[assembly: AssemblyFileVersion("0.1.0.0")] +[assembly: AssemblyVersion("0.1.0.0")] +[assembly: AssemblyInformationalVersion("0.1.0+0.Branch.main.Sha.ae43f4c7111e09d2a34d881c6704a2dea81d9155")] diff --git a/src/Messaging/Common/BlockStorageInfo.cs b/src/Messaging/Common/BlockStorageInfo.cs new file mode 100644 index 0000000..af8a9db --- /dev/null +++ b/src/Messaging/Common/BlockStorageInfo.cs @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: © 2021-2022 MONAI Consortium +// SPDX-License-Identifier: Apache License 2.0 + +using Newtonsoft.Json; + +namespace Monai.Deploy.Messaging.Common +{ + public class BlockStorageInfo + { + /// + /// Gets or sets the root path to the file. + /// + [JsonProperty(PropertyName = "path")] + public string Path { get; set; } = default!; + + /// + /// Gets or sets the root path to the metadata file. + /// + [JsonProperty(PropertyName = "metadata")] + public string Metadata { get; set; } = default!; + } +} diff --git a/src/Messaging/Common/Log.cs b/src/Messaging/Common/Log.cs new file mode 100644 index 0000000..dfb4098 --- /dev/null +++ b/src/Messaging/Common/Log.cs @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: © 2022 MONAI Consortium +// SPDX-License-Identifier: Apache License 2.0 + +using Microsoft.Extensions.Logging; + +namespace Monai.Deploy.Messaging.Common +{ + public static partial class Log + { + internal static readonly string LoggingScopeMessageApplication = "Message ID={0}. Application ID={1}."; + + [LoggerMessage(EventId = 10000, Level = LogLevel.Information, Message = "Publishing message to {endpoint}/{virtualHost}. Exchange={exchange}, Routing Key={topic}.")] + public static partial void PublshingRabbitMq(this ILogger logger, string endpoint, string virtualHost, string exchange, string topic); + + [LoggerMessage(EventId = 10001, Level = LogLevel.Information, Message = "{ServiceName} connecting to {endpoint}/{virtualHost}.")] + public static partial void ConnectingToRabbitMq(this ILogger logger, string serviceNAme, string endpoint, string virtualHost); + + [LoggerMessage(EventId = 10002, Level = LogLevel.Information, Message = "Message received from queue {queue} for {topic}.")] + public static partial void MessageReceivedFromQueue(this ILogger logger, string queue, string topic); + + [LoggerMessage(EventId = 10003, Level = LogLevel.Information, Message = "Listening for messages from {endpoint}/{virtualHost}. Exchange={exchange}, Queue={queue}, Routing Key={topic}.")] + public static partial void SubscribeToRabbitMqQueue(this ILogger logger, string endpoint, string virtualHost, string exchange, string queue, string topic); + + [LoggerMessage(EventId = 10004, Level = LogLevel.Information, Message = "Sending message acknowledgement for message {messageId}.")] + public static partial void SendingAcknowledgement(this ILogger logger, string messageId); + + [LoggerMessage(EventId = 10005, Level = LogLevel.Information, Message = "Ackowledge sent for message {messageId}.")] + public static partial void AcknowledgementSent(this ILogger logger, string messageId); + + [LoggerMessage(EventId = 10006, Level = LogLevel.Information, Message = "Sending nack message {messageId} and requeuing.")] + public static partial void SendingNAcknowledgement(this ILogger logger, string messageId); + + [LoggerMessage(EventId = 10007, Level = LogLevel.Information, Message = "Nack message sent for message {messageId}.")] + public static partial void NAcknowledgementSent(this ILogger logger, string messageId); + + [LoggerMessage(EventId = 10008, Level = LogLevel.Information, Message = "Closing connection.")] + public static partial void ClosingConnection(this ILogger logger); + } +} diff --git a/src/Messaging/Common/MessageReceivedEventArgs.cs b/src/Messaging/Common/MessageReceivedEventArgs.cs new file mode 100644 index 0000000..e7c866a --- /dev/null +++ b/src/Messaging/Common/MessageReceivedEventArgs.cs @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: © 2021-2022 MONAI Consortium +// SPDX-License-Identifier: Apache License 2.0 + +using Monai.Deploy.Messaging.Messages; + +namespace Monai.Deploy.Messaging.Common +{ + /// + /// Provides data for the subscribed event from a message broker. + /// + public class MessageReceivedEventArgs : EventArgs + { + public Message Message { get; } + public CancellationToken CancellationToken { get; } + + public MessageReceivedEventArgs(Message message, CancellationToken cancellationToken) + { + Message = message; + CancellationToken = cancellationToken; + } + } +} diff --git a/src/Messaging/Configuration/ConfigurationException.cs b/src/Messaging/Configuration/ConfigurationException.cs new file mode 100644 index 0000000..1da9311 --- /dev/null +++ b/src/Messaging/Configuration/ConfigurationException.cs @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: © 2021-2022 MONAI Consortium +// SPDX-License-Identifier: Apache License 2.0 + +using System.Runtime.Serialization; + +namespace Monai.Deploy.Messaging.Configuration +{ + [Serializable] + public class ConfigurationException : Exception + { + public ConfigurationException() + { + } + + public ConfigurationException(string? message) : base(message) + { + } + + public ConfigurationException(string? message, Exception? innerException) : base(message, innerException) + { + } + + protected ConfigurationException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/src/Messaging/Configuration/ConfigurationKeys.cs b/src/Messaging/Configuration/ConfigurationKeys.cs new file mode 100644 index 0000000..367b25c --- /dev/null +++ b/src/Messaging/Configuration/ConfigurationKeys.cs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: © 2021-2022 MONAI Consortium +// SPDX-License-Identifier: Apache License 2.0 + +namespace Monai.Deploy.Messaging.Configuration +{ + internal static class ConfigurationKeys + { + public static readonly string EndPoint = "endpoint"; + public static readonly string Username = "username"; + public static readonly string Password = "password"; + public static readonly string VirtualHost = "virtualHost"; + public static readonly string Exchange = "exchange"; + public static readonly string ExportRequestQueue = "exportRequestQueue"; + + public static readonly string[] PublisherRequiredKeys = new[] { EndPoint, Username, Password, VirtualHost, Exchange }; + public static readonly string[] SubscriberRequiredKeys = new[] { EndPoint, Username, Password, VirtualHost, Exchange, ExportRequestQueue }; + } +} diff --git a/src/Messaging/Configuration/MessageBrokerServiceConfiguration.cs b/src/Messaging/Configuration/MessageBrokerServiceConfiguration.cs new file mode 100644 index 0000000..a055d9e --- /dev/null +++ b/src/Messaging/Configuration/MessageBrokerServiceConfiguration.cs @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: © 2022 MONAI Consortium +// SPDX-License-Identifier: Apache License 2.0 + +using Microsoft.Extensions.Configuration; + +namespace Monai.Deploy.Messaging.Configuration +{ + public class MessageBrokerServiceConfiguration + { + /// + /// Gets or sets the a fully qualified type name of the message publisher service. + /// The spcified type must implement IMessageBrokerPublisherService interface. + /// The default message publisher service configured is RabbitMQ. + /// + [ConfigurationKeyName("publisherServiceAssemblyName")] + public string PublisherServiceAssemblyName { get; set; } = "Monai.Deploy.Messaging.RabbitMq.RabbitMqMessagePublisherService, Monai.Deploy.Messaging"; + + /// + /// Gets or sets the a fully qualified type name of the message subscriber service. + /// The spcified type must implement IMessageBrokerSubscriberService interface. + /// The default message subscriber service configured is RabbitMQ. + /// + [ConfigurationKeyName("subscriberServiceAssemblyName")] + public string SubscriberServiceAssemblyName { get; set; } = "Monai.Deploy.Messaging.RabbitMq.RabbitMqMessageSubscriberService, Monai.Deploy.Messaging"; + + /// + /// Gets or sets the message publisher specific settings. + /// Service implementer shall validate settings in the constructor and specify all settings in a single level JSON object as in the example below. + /// + /// + /// + /// { + /// ... + /// "publisherSettings": { + /// "endpoint": "1.2.3.4", + /// "username": "monaideploy", + /// "password": "mysecret", + /// "setting-a": "value-a", + /// "setting-b": "value-b" + /// } + /// } + /// + /// + [ConfigurationKeyName("publisherSettings")] + public Dictionary PublisherSettings { get; set; } = new Dictionary(); + + /// + /// Gets or sets the message subscriber specific settings. + /// Service implementer shall validate settings in the constructor and specify all settings in a single level JSON object as in the example below. + /// + /// + /// + /// { + /// ... + /// "subscriberSettings": { + /// "endpoint": "1.2.3.4", + /// "username": "monaideploy", + /// "password": "myothersecret", + /// "setting-a": "value-a", + /// "setting-b": "value-b" + /// } + /// } + /// + /// + [ConfigurationKeyName("subscriberSettings")] + public Dictionary SubscriberSettings { get; set; } = new Dictionary(); + } +} diff --git a/src/Messaging/Messages/ExportCompleteMessage.cs b/src/Messaging/Messages/ExportCompleteMessage.cs new file mode 100644 index 0000000..d907676 --- /dev/null +++ b/src/Messaging/Messages/ExportCompleteMessage.cs @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: © 2021-2022 MONAI Consortium +// SPDX-License-Identifier: Apache License 2.0 + +using Ardalis.GuardClauses; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace Monai.Deploy.Messaging.Messages +{ + public enum ExportStatus + { + Success = 0, + Failure, + PartialFailure, + Unknown + } + + public class ExportCompleteMessage + { + /// + /// Gets or sets the workflow ID generated by the Workflow Manager. + /// + public string WorkflowId { get; set; } = default!; + + /// + /// Gets or sets the export task ID generated by the Workflow Manager. + /// + public string ExportTaskId { get; set; } = default!; + + /// + /// Gets or sets the state of the export task. + /// + [JsonConverter(typeof(StringEnumConverter))] + public ExportStatus Status { get; set; } + + /// + /// Gets or sets error messages, if any, when exporting. + /// + public string Message { get; set; } = default!; + + [JsonConstructor] + public ExportCompleteMessage() + { + Status = ExportStatus.Unknown; + } + + public ExportCompleteMessage(ExportRequestMessage exportRequest) + { + Guard.Against.Null(exportRequest, nameof(exportRequest)); + + WorkflowId = exportRequest.WorkflowId; + ExportTaskId = exportRequest.ExportTaskId; + Message = string.Join(System.Environment.NewLine, exportRequest.ErrorMessages); + + if (exportRequest.FailedFiles == 0) + { + Status = ExportStatus.Success; + } + else if (exportRequest.FailedFiles == exportRequest.Files.Count()) + { + Status = ExportStatus.Failure; + } + else + { + Status = ExportStatus.PartialFailure; + } + } + } +} diff --git a/src/Messaging/Messages/ExportRequestMessage.cs b/src/Messaging/Messages/ExportRequestMessage.cs new file mode 100644 index 0000000..bff2b3d --- /dev/null +++ b/src/Messaging/Messages/ExportRequestMessage.cs @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: © 2021-2022 MONAI Consortium +// SPDX-License-Identifier: Apache License 2.0 + +namespace Monai.Deploy.Messaging.Messages +{ + public class ExportRequestMessage + { + /// + /// Gets or sets the workflow ID generated by the Workflow Manager. + /// + public string WorkflowId { get; set; } = default!; + + /// + /// Gets or sets the export task ID generated by the Workflow Manager. + /// + public string ExportTaskId { get; set; } = default!; + + /// + /// Gets or sets a list of files to be exported. + /// + public IEnumerable Files { get; set; } = default!; + + /// + /// Gets or sets the export target. + /// For DIMSE, the named DICOM destination. + /// For ACR, the Transaction ID in the original inference request. + /// + public string Destination { get; set; } = default!; + + /// + /// Gets or set the correlation ID. + /// For DIMSE, the correlation ID is the UUID associated with the first DICOM association received. + /// For ACR, use the Transaction ID in the original request. + /// + public string CorrelationId { get; set; } = default!; + + /// + /// Gets or set number of files exported successfully. + /// + public int SucceededFiles { get; set; } = 0; + + /// + /// Gets or sets number of files failed to export. + /// + public int FailedFiles { get; set; } = 0; + + /// + /// Gets or sets the delivery tag or acknowledge token for the task. + /// + public string DeliveryTag { get; set; } = default!; + + /// + /// Gets or sets the message ID set by the message broker. + /// + public string MessageId { get; set; } = default!; + + /// + /// Gets whether the export task is completed or not based on file count. + /// + public bool IsCompleted + { get { return (SucceededFiles + FailedFiles) == Files.Count(); } } + + /// + /// Gets or sets error messages related to this export task. + /// + public List ErrorMessages { get; init; } + + public ExportRequestMessage() + { + ErrorMessages = new List(); + } + + public void AddErrorMessages(IList errorMessages) + { + ErrorMessages.AddRange(errorMessages); + } + } +} diff --git a/src/Messaging/Messages/JsonMessage.cs b/src/Messaging/Messages/JsonMessage.cs new file mode 100644 index 0000000..928b9a3 --- /dev/null +++ b/src/Messaging/Messages/JsonMessage.cs @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: © 2021-2022 MONAI Consortium +// SPDX-License-Identifier: Apache License 2.0 + +using System.Net.Mime; +using System.Text; +using Ardalis.GuardClauses; +using Newtonsoft.Json; + +namespace Monai.Deploy.Messaging.Messages +{ + public sealed class JsonMessage : MessageBase + { + /// + /// Body of the message. + /// + public T Body { get; init; } + + public JsonMessage(T body, + string applicationId, + string correlationId, + string deliveryTag = "") + : this(body, + body?.GetType().Name ?? default!, + Guid.NewGuid().ToString(), + applicationId, + correlationId, + DateTime.UtcNow, + deliveryTag) + { + } + + public JsonMessage(T body, + string messageDescription, + string messageId, + string applicationId, + string correlationId, + DateTime creationDateTime, + string deliveryTag) + : base(messageId, messageDescription, MediaTypeNames.Application.Json, applicationId, correlationId, creationDateTime) + { + Guard.Against.Null(body, nameof(body)); + + Body = body; + DeliveryTag = deliveryTag; + } + + /// + /// Converts Body to JSON and then binary[]. + /// + /// + public Message ToMessage() + { + var json = JsonConvert.SerializeObject(Body); + + return new Message( + Encoding.UTF8.GetBytes(json), + Body?.GetType().Name ?? default!, + MessageId, + ApplicationId, + ContentType, + CorrelationId, + CreationDateTime, + DeliveryTag); + } + } +} diff --git a/src/Messaging/Messages/Message.cs b/src/Messaging/Messages/Message.cs new file mode 100644 index 0000000..4a8c996 --- /dev/null +++ b/src/Messaging/Messages/Message.cs @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: © 2021-2022 MONAI Consortium +// SPDX-License-Identifier: Apache License 2.0 + +using System.Text; +using Newtonsoft.Json; + +namespace Monai.Deploy.Messaging.Messages +{ + public sealed class Message : MessageBase + { + /// + /// Body of the message. + /// + public byte[] Body { get; init; } + + public Message(byte[] body, + string messageDescription, + string messageId, + string applicationId, + string contentType, + string correlationId, + DateTime creationDateTime, + string deliveryTag = "") + : base(messageId, messageDescription, contentType, applicationId, correlationId, creationDateTime) + { + Body = body; + DeliveryTag = deliveryTag; + } + + /// + /// Converts Body from binary[] to JSON string and then the specified T type. + /// + /// Type to convert to + /// Instance of T or null if data cannot be deserialized. + public T ConvertTo() + { + var json = Encoding.UTF8.GetString(Body); + return JsonConvert.DeserializeObject(json)!; + } + } +} diff --git a/src/Messaging/Messages/MessageBase.cs b/src/Messaging/Messages/MessageBase.cs new file mode 100644 index 0000000..3cbefa7 --- /dev/null +++ b/src/Messaging/Messages/MessageBase.cs @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: © 2022 MONAI Consortium +// SPDX-License-Identifier: Apache License 2.0 + +using Ardalis.GuardClauses; + +namespace Monai.Deploy.Messaging.Messages +{ + public abstract class MessageBase + { + /// + /// UUID for the message formatted with hyphens. + /// xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx + /// + public string MessageId { get; init; } + + /// + /// A short description of the type serialized in the message body. + /// + public string MessageDescription { get; init; } + + /// + /// Content or MIME type of the message body. + /// + public string ContentType { get; init; } + + /// + /// UUID of the application, in this case, the Informatics Gateway. + /// The UUID of Informatics Gateway is 16988a78-87b5-4168-a5c3-2cfc2bab8e54. + /// + public string ApplicationId { get; init; } + + /// + /// Correlation ID of the message. + /// For DIMSE connections, the ID generated during association is used. + /// For ACR inference requests, the Transaction ID provided in the request is used. + /// + public string CorrelationId { get; init; } + + /// + /// Datetime the message is created. + /// + public DateTime CreationDateTime { get; init; } + + /// + /// Gets or set the delivery tag/acknoweldge token for the message. + /// + public string DeliveryTag { get; init; } + + protected MessageBase(string messageId, + string messageDescription, + string contentType, + string applicationId, + string correlationId, + DateTime creationDateTime) + { + Guard.Against.NullOrWhiteSpace(messageId, nameof(messageId)); + Guard.Against.NullOrWhiteSpace(messageDescription, nameof(messageDescription)); + Guard.Against.NullOrWhiteSpace(contentType, nameof(contentType)); + Guard.Against.NullOrWhiteSpace(applicationId, nameof(applicationId)); + Guard.Against.NullOrWhiteSpace(correlationId, nameof(correlationId)); + + MessageId = messageId; + MessageDescription = messageDescription; + ContentType = contentType; + ApplicationId = applicationId; + CorrelationId = correlationId; + CreationDateTime = creationDateTime; + DeliveryTag = string.Empty; + } + } +} diff --git a/src/Messaging/Messages/WorkflowRequestMessage.cs b/src/Messaging/Messages/WorkflowRequestMessage.cs new file mode 100644 index 0000000..9b96a1a --- /dev/null +++ b/src/Messaging/Messages/WorkflowRequestMessage.cs @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: © 2021-2022 MONAI Consortium +// SPDX-License-Identifier: Apache License 2.0 + +using Monai.Deploy.Messaging.Common; +using Newtonsoft.Json; + +namespace Monai.Deploy.Messaging.Messages +{ + public class WorkflowRequestMessage + { + private readonly List _payload; + + /// + /// Gets or sets the ID of the payload which is also used as the root path of the payload. + /// + [JsonProperty(PropertyName = "payload_id")] + public Guid PayloadId { get; set; } + + /// + /// Gets or sets the associated workflows to be launched. + /// + [JsonProperty(PropertyName = "workflows")] + public IEnumerable Workflows { get; set; } + + /// + /// Gets or sets number of files in the payload. + /// + [JsonProperty(PropertyName = "file_count")] + public int FileCount { get; set; } + + /// + /// For DIMSE, the correlation ID is the UUID associated with the first DICOM association received. + /// For an ACR inference request, the correlation ID is the Transaction ID in the original request. + /// + [JsonProperty(PropertyName = "correlation_id")] + public string CorrelationId { get; set; } = default!; + + /// + /// Gets or set the name of the bucket where the files in are stored. + /// + [JsonProperty(PropertyName = "bucket")] + public string Bucket { get; set; } = default!; + + /// + /// For DIMSE, the sender or calling AE Title of the DICOM dataset. + /// For an ACR inference request, the transaction ID. + /// + [JsonProperty(PropertyName = "calling_aetitle")] + public string CallingAeTitle { get; set; } = default!; + + /// + /// For DIMSE, the MONAI Deploy AE Title received the DICOM dataset. + /// For an ACR inference request, this field is empty. + /// + [JsonProperty(PropertyName = "called_aetitle")] + public string CalledAeTitle { get; set; } = default!; + + /// + /// Gets or sets the time the data was received. + /// + [JsonProperty(PropertyName = "timestamp")] + public DateTime Timestamp { get; set; } + + /// + /// Gets or sets a list of files and metadata files in this request. + /// + [JsonProperty(PropertyName = "payload")] + public IReadOnlyList Payload { get => _payload; } + + public WorkflowRequestMessage() + { + _payload = new List(); + Workflows = new List(); + } + + public void AddFiles(IEnumerable files) + { + _payload.AddRange(files); + } + } +} diff --git a/src/Messaging/Monai.Deploy.Messaging.csproj b/src/Messaging/Monai.Deploy.Messaging.csproj new file mode 100644 index 0000000..d6931a7 --- /dev/null +++ b/src/Messaging/Monai.Deploy.Messaging.csproj @@ -0,0 +1,54 @@ + + + + + net6.0 + enable + enable + false + ..\.sonarlint\project-monai_monai-deploy-messagingcsharp.ruleset + + + + Monai.Deploy.Messaging + 0.1.0 + MONAI Consortium + MONAI Consortium + true + MONAI Deploy communication system between clinical data pipelines components. + MONAI Consortium + https://github.com/Project-MONAI/monai-deploy-messaging + README.md + https://github.com/Project-MONAI/monai-deploy-messaging/ + Apache-2.0 + True + + + + + + + + + + + + + + + True + \ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Messaging/RabbitMq/RabbitMqMessagePublisherService.cs b/src/Messaging/RabbitMq/RabbitMqMessagePublisherService.cs new file mode 100644 index 0000000..d22397e --- /dev/null +++ b/src/Messaging/RabbitMq/RabbitMqMessagePublisherService.cs @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: © 2021-2022 MONAI Consortium +// SPDX-License-Identifier: Apache License 2.0 + +using System.Globalization; +using Ardalis.GuardClauses; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.Messaging.Common; +using Monai.Deploy.Messaging.Configuration; +using Monai.Deploy.Messaging.Messages; +using RabbitMQ.Client; + +namespace Monai.Deploy.Messaging.RabbitMq +{ + public class RabbitMqMessagePublisherService : IMessageBrokerPublisherService, IDisposable + { + private readonly ILogger _logger; + private readonly string _endpoint; + private readonly string _virtualHost; + private readonly string _exchange; + private readonly IConnection _connection; + private bool _disposedValue; + + public string Name => "Rabbit MQ Publisher"; + + public RabbitMqMessagePublisherService(IOptions options, + ILogger logger) + { + Guard.Against.Null(options, nameof(options)); + + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + var configuration = options.Value; + ValidateConfiguration(configuration); + _endpoint = configuration.PublisherSettings[ConfigurationKeys.EndPoint]; + var username = configuration.PublisherSettings[ConfigurationKeys.Username]; + var password = configuration.PublisherSettings[ConfigurationKeys.Password]; + _virtualHost = configuration.SubscriberSettings[ConfigurationKeys.VirtualHost]; + _exchange = configuration.SubscriberSettings[ConfigurationKeys.Exchange]; + + var connectionFactory = new ConnectionFactory() + { + HostName = _endpoint, + UserName = username, + Password = password, + VirtualHost = _virtualHost + }; + _connection = connectionFactory.CreateConnection(); + } + + private void ValidateConfiguration(MessageBrokerServiceConfiguration configuration) + { + Guard.Against.Null(configuration, nameof(configuration)); + Guard.Against.Null(configuration.PublisherSettings, nameof(configuration.PublisherSettings)); + + foreach (var key in ConfigurationKeys.PublisherRequiredKeys) + { + if (!configuration.PublisherSettings.ContainsKey(key)) + { + throw new ConfigurationException($"{Name} is missing configuration for {key}."); + } + } + } + + public Task Publish(string topic, Message message) + { + Guard.Against.NullOrWhiteSpace(topic, nameof(topic)); + Guard.Against.Null(message, nameof(message)); + + using var loggerScope = _logger.BeginScope(string.Format(CultureInfo.InvariantCulture, Log.LoggingScopeMessageApplication, message.MessageId, message.ApplicationId)); + + _logger.PublshingRabbitMq(_endpoint, _virtualHost, _exchange, topic); + + using var channel = _connection.CreateModel(); + channel.ExchangeDeclare(_exchange, ExchangeType.Topic); + + var propertiesDictionary = new Dictionary + { + { "CreationDateTime", message.CreationDateTime.ToString("o") } + }; + + var properties = channel.CreateBasicProperties(); + properties.Persistent = true; + properties.ContentType = message.ContentType; + properties.MessageId = message.MessageId; + properties.AppId = message.ApplicationId; + properties.CorrelationId = message.CorrelationId; + properties.DeliveryMode = 2; + + properties.Headers = propertiesDictionary; + channel.BasicPublish(exchange: _exchange, + routingKey: topic, + basicProperties: properties, + body: message.Body); + + return Task.CompletedTask; + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing && _connection != null) + { + _logger.ClosingConnection(); + _connection.Close(); + _connection.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Messaging/RabbitMq/RabbitMqMessageSubscriberService.cs b/src/Messaging/RabbitMq/RabbitMqMessageSubscriberService.cs new file mode 100644 index 0000000..dcd6207 --- /dev/null +++ b/src/Messaging/RabbitMq/RabbitMqMessageSubscriberService.cs @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: © 2021-2022 MONAI Consortium +// SPDX-License-Identifier: Apache License 2.0 + +using System.Globalization; +using System.Text; +using Ardalis.GuardClauses; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.Messaging.Common; +using Monai.Deploy.Messaging.Configuration; +using Monai.Deploy.Messaging.Messages; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace Monai.Deploy.Messaging.RabbitMq +{ + public class RabbitMqMessageSubscriberService : IMessageBrokerSubscriberService, IDisposable + { + private readonly ILogger _logger; + private readonly string _endpoint; + private readonly string _virtualHost; + private readonly string _exchange; + private readonly IConnection _connection; + private readonly IModel _channel; + private bool _disposedValue; + + public string Name => "Rabbit MQ Subscriber"; + + public RabbitMqMessageSubscriberService(IOptions options, + ILogger logger) + { + Guard.Against.Null(options, nameof(options)); + + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + var configuration = options.Value; + ValidateConfiguration(configuration); + _endpoint = configuration.SubscriberSettings[ConfigurationKeys.EndPoint]; + var username = configuration.SubscriberSettings[ConfigurationKeys.Username]; + var password = configuration.SubscriberSettings[ConfigurationKeys.Password]; + _virtualHost = configuration.SubscriberSettings[ConfigurationKeys.VirtualHost]; + _exchange = configuration.SubscriberSettings[ConfigurationKeys.Exchange]; + + var connectionFactory = new ConnectionFactory() + { + HostName = _endpoint, + UserName = username, + Password = password, + VirtualHost = _virtualHost + }; + + _logger.ConnectingToRabbitMq(Name, _endpoint, _virtualHost); + _connection = connectionFactory.CreateConnection(); + _channel = _connection.CreateModel(); + _channel.ExchangeDeclare(_exchange, ExchangeType.Topic); + _channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false); + } + + private void ValidateConfiguration(MessageBrokerServiceConfiguration configuration) + { + Guard.Against.Null(configuration, nameof(configuration)); + Guard.Against.Null(configuration.SubscriberSettings, nameof(configuration.SubscriberSettings)); + + foreach (var key in ConfigurationKeys.SubscriberRequiredKeys) + { + if (!configuration.SubscriberSettings.ContainsKey(key)) + { + throw new ConfigurationException($"{Name} is missing configuration for {key}."); + } + } + } + + public void Subscribe(string topic, string queue, Action messageReceivedCallback, ushort prefetchCount = 0) + { + Guard.Against.NullOrWhiteSpace(topic, nameof(topic)); + Guard.Against.Null(messageReceivedCallback, nameof(messageReceivedCallback)); + + var queueDeclareResult = _channel.QueueDeclare(queue: queue, durable: true, exclusive: false, autoDelete: false); + _channel.QueueBind(queueDeclareResult.QueueName, _exchange, topic); + + var consumer = new EventingBasicConsumer(_channel); + consumer.Received += (model, eventArgs) => + { + using var loggerScope = _logger.BeginScope(string.Format(CultureInfo.InvariantCulture, Log.LoggingScopeMessageApplication, eventArgs.BasicProperties.MessageId, eventArgs.BasicProperties.AppId)); + + _logger.MessageReceivedFromQueue(queueDeclareResult.QueueName, topic); + + var messageReceivedEventArgs = new MessageReceivedEventArgs( + new Message( + body: eventArgs.Body.ToArray(), + messageDescription: topic, + messageId: eventArgs.BasicProperties.MessageId, + applicationId: eventArgs.BasicProperties.AppId, + contentType: eventArgs.BasicProperties.ContentType, + correlationId: eventArgs.BasicProperties.CorrelationId, + creationDateTime: DateTime.Parse(Encoding.UTF8.GetString((byte[])eventArgs.BasicProperties.Headers["CreationDateTime"]), CultureInfo.InvariantCulture), + deliveryTag: eventArgs.DeliveryTag.ToString(CultureInfo.InvariantCulture)), + new CancellationToken()); + + messageReceivedCallback(messageReceivedEventArgs); + }; + _channel.BasicQos(0, prefetchCount, false); + _channel.BasicConsume(queueDeclareResult.QueueName, false, consumer); + _logger.SubscribeToRabbitMqQueue(_endpoint, _virtualHost, _exchange, queueDeclareResult.QueueName, topic); + } + + public void Acknowledge(MessageBase message) + { + Guard.Against.Null(message, nameof(message)); + + _logger.SendingAcknowledgement(message.MessageId); + _channel.BasicAck(ulong.Parse(message.DeliveryTag, CultureInfo.InvariantCulture), multiple: false); + _logger.AcknowledgementSent(message.MessageId); + } + + public void Reject(MessageBase message) + { + Guard.Against.Null(message, nameof(message)); + + _logger.SendingNAcknowledgement(message.MessageId); + _channel.BasicNack(ulong.Parse(message.DeliveryTag, CultureInfo.InvariantCulture), multiple: false, requeue: true); + _logger.NAcknowledgementSent(message.MessageId); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing && _connection is not null) + { + _logger.ClosingConnection(); + _connection.Close(); + _connection.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Messaging/Test/DummyTest.cs b/src/Messaging/Test/DummyTest.cs new file mode 100644 index 0000000..1e16c86 --- /dev/null +++ b/src/Messaging/Test/DummyTest.cs @@ -0,0 +1,13 @@ +using Xunit; + +namespace Monai.Deploy.Messaging.Test +{ + public class DummyTest + { + [Fact] + public void ToBeDeleted() + { + Assert.True(true); + } + } +} diff --git a/src/Messaging/Test/Monai.Deploy.Messaging.Test.csproj b/src/Messaging/Test/Monai.Deploy.Messaging.Test.csproj new file mode 100644 index 0000000..9f77f5f --- /dev/null +++ b/src/Messaging/Test/Monai.Deploy.Messaging.Test.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + enable + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/src/Monai.Deploy.Messaging.sln b/src/Monai.Deploy.Messaging.sln new file mode 100644 index 0000000..eaf1c48 --- /dev/null +++ b/src/Monai.Deploy.Messaging.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.32210.238 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monai.Deploy.Messaging", "Messaging\Monai.Deploy.Messaging.csproj", "{0EF89B52-A68A-4F5A-B5C6-49D6AC3582C8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A73CC54A-00FC-47AA-A372-9F3E5265A5C1}" + ProjectSection(SolutionItems) = preProject + AssemblyInfo.cs = AssemblyInfo.cs + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monai.Deploy.Messaging.Test", "Messaging\Test\Monai.Deploy.Messaging.Test.csproj", "{33516A8E-BCBF-4B63-A358-C3A3B03F63A5}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0EF89B52-A68A-4F5A-B5C6-49D6AC3582C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0EF89B52-A68A-4F5A-B5C6-49D6AC3582C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0EF89B52-A68A-4F5A-B5C6-49D6AC3582C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0EF89B52-A68A-4F5A-B5C6-49D6AC3582C8}.Release|Any CPU.Build.0 = Release|Any CPU + {33516A8E-BCBF-4B63-A358-C3A3B03F63A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {33516A8E-BCBF-4B63-A358-C3A3B03F63A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {33516A8E-BCBF-4B63-A358-C3A3B03F63A5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {33516A8E-BCBF-4B63-A358-C3A3B03F63A5}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {E1105263-9CBF-45AA-BAC3-BD8504C1B962} + EndGlobalSection +EndGlobal diff --git a/src/coverlet.runsettings b/src/coverlet.runsettings new file mode 100644 index 0000000..561469f --- /dev/null +++ b/src/coverlet.runsettings @@ -0,0 +1,21 @@ + + + + + + + opencover + [coverlet.*.tests?]*,[*]Coverlet.Core*,[xunit.*]*,[Grpc.Core*]*,[System.*]*,[Microsoft.*]* + + + + + false + true + true + true + + + + +